tebako 0.15.1 → 0.15.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 349d5b0a59f98021f05bae37a7c62efd9282900f5d4c161e8567424a082d21a1
4
- data.tar.gz: 74dabe977337bcbe8b63a82cc82653242e852542e629a8698f3f6828c59cebf3
3
+ metadata.gz: 8ec3da7722b76d52f17d341664974ff8ea5f5164feec924958cd2725238439d0
4
+ data.tar.gz: 2e244d8b29c27a4f884aa969961dca6968323d21376012bf539dbce28c5b7baa
5
5
  SHA512:
6
- metadata.gz: eed8e60a9fe1c1185478c7f9c70d4a4e1af7c5f24c853c6a1ac85527c04bb7b281c5efdbc3cb9afd2ee31dd56e4d5a6d0e7975388ea180dc03d93fa4d703b845
7
- data.tar.gz: 1355806d02bbe169d73d6ca746a5052d24dba4f49c398f514d25d8c35592b2d451f97629e60801d656ad755b70356d201a1211dd882a611fe1a083900b28c0e1
6
+ metadata.gz: ccf67b3e4bf6444ca31627b27e9876699f743cdac01e2ebea0cee29a9ea8113d24d5ef34d6b15ebd8461620c8e29bb3d75ef7332b5a1934e3b3d579130cdb871
7
+ data.tar.gz: 51695b16c05b1063678bc2f4c6042ae65c3111799f14f81a5d3982a30c4f94f74f7f28d2fa587c11cdcf509ff9a5f105428ed95eff693d47377337980d349177
data/CMakeLists.txt CHANGED
@@ -175,7 +175,7 @@ string(CONCAT RUBY_API_VER ${RUBY_VER_BASE} ".0")
175
175
  # NOTE: the env override in def_ext_prj_g (tebako-tools) is currently broken
176
176
  # (`if (DEFINED ENV{${TAG})` is missing a closing brace, so the test never
177
177
  # fires); this default is the operative tag selection.
178
- def_ext_prj_g(DWARFS_WR "v0.12.8")
178
+ def_ext_prj_g(DWARFS_WR "v0.12.9")
179
179
 
180
180
  # Provisions libtfs (prebuilt package or vcpkg toolchain for the source
181
181
  # build), the prebuilt mkdwarfs, and -- for the prebuilt path -- the
@@ -217,6 +217,7 @@ endif()
217
217
  def_ext_prj_g(PATCHELF "65e14792061c298f1d2bc44becd48a10cbf0bc81")
218
218
 
219
219
  set(LIBYAML_RUBY_OPTION "")
220
+ set(RUBY_LIBS "")
220
221
  if(${RUBY_VER} VERSION_LESS "3.2.0")
221
222
  set(LIBYAML_RUBY_OPTION "--enable-bundled-libyaml")
222
223
  endif(${RUBY_VER} VERSION_LESS "3.2.0")
@@ -397,6 +398,16 @@ else(IS_DARWIN)
397
398
  if(RB_W32)
398
399
  string(CONCAT RUBY_C_FLAGS ${RUBY_C_FLAGS} " -DRB_W32=1")
399
400
  string(CONCAT RUBY_L_FLAGS ${RUBY_L_FLAGS} " -l:libstdc++.a -L${TLIBD}")
401
+ # vcpkg's static libcrypto.a (the CAPI engine, e_capi.obj) references
402
+ # the CryptoAPI certificate store (CertFreeCertificateContext & co.),
403
+ # but ruby's default mingw $LIBS lack -lcrypt32, so every ext/openssl
404
+ # have_func link probe failed with undefined Cert* references and the
405
+ # OpenSSL-1.1.0 accessor fallbacks in openssl_missing.c compiled
406
+ # against opaque 3.x structs (tebako#345). mkmf appends $LIBS after
407
+ # -lssl/-lcrypto, so LIBS here lands in the right order for static
408
+ # linking; advapi32/user32 cover the rest of the usual static-openssl
409
+ # system-lib tail.
410
+ set(RUBY_LIBS "-lcrypt32 -ladvapi32 -luser32")
400
411
  set(C_FLAGS_DEST cppflags)
401
412
  endif(RB_W32)
402
413
 
@@ -432,7 +443,8 @@ ExternalProject_Add(${RUBY_PRJ}
432
443
  --with-out-ext=${RUBY_WITHOUT_EXT} \
433
444
  --prefix=${DATA_SRC_DIR} \
434
445
  ${C_FLAGS_DEST}=\"${RUBY_C_FLAGS}\" \
435
- LDFLAGS=\"${RUBY_L_FLAGS}\""
446
+ LDFLAGS=\"${RUBY_L_FLAGS}\" \
447
+ LIBS=\"${RUBY_LIBS}\""
436
448
  COMMAND ruby ${EXE}/tebako-packager pass2 ${OSTYPE_TXT} ${RUBY_SOURCE_DIR} ${DEPS_LIB_DIR} ${DATA_SRC_DIR} ${RUBY_STASH_DIR} ${RUBY_VER}
437
449
  INSTALL_COMMAND ""
438
450
  )
data/README.adoc CHANGED
@@ -420,7 +420,7 @@ tebako press \
420
420
  [-c|--cwd=<package-current-working-directory>] \
421
421
  [-D|--devmode] \
422
422
  [-P|--patchelf] \
423
- [-m|--mode=<bundle|both|application|runtime>] \
423
+ [-m|--mode=<lean|fat|classic|bundle|both|application|runtime>] \
424
424
  [-u|--ref=<runtime-reference>] \
425
425
  [-t|--tebafile=<path-to-tebafile>]
426
426
  ----
@@ -452,6 +452,7 @@ On Windows, the output file is automatically appended the `.exe` extension.
452
452
 
453
453
  *** in `runtime` mode, the runtime package is created at `tebako-runtime`.
454
454
  *** in `application` mode, the application package is created at `<current-folder>/<entry-point-base-name>`.
455
+ *** in `lean`, `fat` and `classic` modes, the package is created at `<current-folder>/<entry-point-base-name>`.
455
456
  *** in `bundle` mode, the bundled package is created at `<current-folder>/<entry-point-base-name>`.
456
457
  *** in `both` mode, the runtime package is created at `<current-folder>/<entry-point-base-name>`,
457
458
  and the application package is created at `<current-folder>/<entry-point-base-name>.tebako`.
@@ -460,6 +461,7 @@ and the application package is created at `<current-folder>/<entry-point-base-na
460
461
 
461
462
  *** in `runtime` mode, the runtime package is named according to the `-o` option.
462
463
  *** in `application` mode, the application package is named according to the `-o` option.
464
+ *** in `lean`, `fat` and `classic` modes, the package is named according to the `-o` option.
463
465
  *** in `bundle` mode, the bundled package is named according to the `-o` option.
464
466
  *** in `both` mode, the runtime package is named according to the `-o` option,
465
467
  and the application package is named according to the `-o` option with the `.tebako` extension.
@@ -502,10 +504,13 @@ For example, a package created at Ubuntu 20 system can be used on Ubuntu 22.
502
504
  NOTE: The feature is exeprimental, we may consider another approach in the future.
503
505
 
504
506
  `--mode=<mode>`::
505
- (optional, defaults to `bundle`)
506
- Package output mode, determines whether the runtime and/or application are
507
- to be separately packaged.
507
+ (optional, defaults to `lean`)
508
+ Package output mode, determines the package layout (three-part or runtime-stitched)
509
+ and whether the runtime and/or application are separately packaged.
508
510
 
511
+ `lean`::: Create a three-part package: the tebako-bootstrap launcher plus the application image slot(s) and a tpkg manifest trailer carrying the runtime reference. At first run the bootstrap resolves (downloads, verifies and caches) the referenced prebuilt runtime into the machine-wide cache (`~/.tebako` or `$TEBAKO_HOME`); subsequent runs of any lean package built for the same runtime share that cache entry. The output file is named according to the `-o` option.
512
+ `fat`::: Like `lean`, but the runtime package is also embedded as a payload slot. The first run installs it into the cache without network access (works with `TEBAKO_OFFLINE=1`). The output file is named according to the `-o` option.
513
+ `classic`::: Stitch the application image onto a prebuilt runtime package (the Stage-3A layout; identical to `bundle` with the default `--runtime prebuilt`). The output file is named according to the `-o` option.
509
514
  `bundle`::: Create a single package bundling both the runtime and application. The output file is named according to the `-o` option.
510
515
  `both`::: Create separate packages for the runtime and application. Outputs two separate files: one for the runtime at the location specified at the `-o {filename}` and one for the application at `{filename}.package`.
511
516
  `application`::: Create the application package only. The output file is named according to the `-o` option.
@@ -82,9 +82,7 @@ endif()
82
82
  if(IS_GNU)
83
83
  set(LIBTFS_PLATFORM "linux-gnu-${__LIBTFS_ARCH}")
84
84
  elseif(IS_MUSL)
85
- if(NOT __LIBTFS_ARCH STREQUAL "x86_64")
86
- message(FATAL_ERROR "No prebuilt libtfs package for linux-musl-${__LIBTFS_ARCH}; use -DDWARFS_PRELOAD=OFF (source build)")
87
- endif()
85
+ # musl-arm64 prebuilt packages first ship in libtfs v0.12.9
88
86
  set(LIBTFS_PLATFORM "linux-musl-${__LIBTFS_ARCH}")
89
87
  elseif(IS_DARWIN)
90
88
  set(LIBTFS_PLATFORM "macos-${__LIBTFS_ARCH}")
@@ -48,7 +48,7 @@
48
48
  * offset size field
49
49
  * 0 8 u64 offset (image start, absolute file offset)
50
50
  * 8 8 u64 size (image length in bytes)
51
- * 16 4 u32 format_id (0=auto(magic), 1=dwarfs, 2=squashfs, 3=zip)
51
+ * 16 4 u32 format_id (0=auto(magic), 1=dwarfs, 2=squashfs, 3=zip, 4=runtime payload)
52
52
  * 20 4 u32 flags
53
53
  * 24 256 char mount_point[256] (UTF-8, NUL-padded)
54
54
  *
@@ -97,8 +97,8 @@ extern "C" {
97
97
  #define TPKG_MOUNT_POINT_LEN 256u
98
98
  #define TPKG_RUNTIME_REF_LEN 128u
99
99
  #define TPKG_MAGIC "TEBAKOTFS"
100
- #define TPKG_MAGIC_LEN 10u /* including the terminating NUL */
101
- #define TPKG_MAGIC_PREFIX_LEN 4u /* "TEBA": absent-vs-corrupt discriminator */
100
+ #define TPKG_MAGIC_LEN 10u /* including the terminating NUL */
101
+ #define TPKG_MAGIC_PREFIX_LEN 4u /* "TEBA": absent-vs-corrupt discriminator */
102
102
 
103
103
  /* package_flags */
104
104
  #define TPKG_FLAG_LEAN 0x1u
@@ -108,6 +108,9 @@ extern "C" {
108
108
  #define TPKG_FORMAT_DWARFS 1u
109
109
  #define TPKG_FORMAT_SQUASHFS 2u
110
110
  #define TPKG_FORMAT_ZIP 3u
111
+ /* A runtime payload slot (fat packages): the compressed language-runtime
112
+ * package the bootstrap installs into the shared cache on first run. */
113
+ #define TPKG_FORMAT_RUNTIME 4u
111
114
 
112
115
  /* ---- error codes (returned by tpkg_errno) -------------------------------- */
113
116
 
@@ -157,7 +160,7 @@ int tpkg_read_mem(const void* data, size_t size, tpkg_manifest* out);
157
160
  int tpkg_write_fd(int fd, const tpkg_manifest* m);
158
161
 
159
162
  /* Magic-independent structural checks: version supported, slot_count in
160
- * 1..TPKG_MAX_SLOTS, offset+size non-overflowing, format_id <= TPKG_FORMAT_ZIP,
163
+ * 1..TPKG_MAX_SLOTS, offset+size non-overflowing, format_id <= TPKG_FORMAT_RUNTIME,
161
164
  * runtime_ref and mount_points NUL-terminated within their fixed fields. */
162
165
  int tpkg_validate(const tpkg_manifest* m);
163
166
 
@@ -220,19 +223,12 @@ enum {
220
223
  };
221
224
 
222
225
  /* slot record field offsets */
223
- enum {
224
- TPKG__REC_OFFSET = 0,
225
- TPKG__REC_SIZE = 8,
226
- TPKG__REC_FORMAT = 16,
227
- TPKG__REC_FLAGS = 20,
228
- TPKG__REC_MOUNT = 24
229
- };
226
+ enum { TPKG__REC_OFFSET = 0, TPKG__REC_SIZE = 8, TPKG__REC_FORMAT = 16, TPKG__REC_FLAGS = 20, TPKG__REC_MOUNT = 24 };
230
227
 
231
228
  /* wire sizes are fixed by the format; catch exotic padding at compile time */
232
229
  typedef char tpkg__assert_slot_size[(sizeof(tpkg_slot) == TPKG_SLOT_SIZE) ? 1 : -1];
233
- typedef char tpkg__assert_manifest_size[(sizeof(tpkg_manifest) ==
234
- 4 * sizeof(uint32_t) + TPKG_RUNTIME_REF_LEN +
235
- TPKG_MAX_SLOTS * sizeof(tpkg_slot))
230
+ typedef char tpkg__assert_manifest_size[(sizeof(tpkg_manifest) == 4 * sizeof(uint32_t) + TPKG_RUNTIME_REF_LEN +
231
+ TPKG_MAX_SLOTS * sizeof(tpkg_slot))
236
232
  ? 1
237
233
  : -1];
238
234
 
@@ -397,7 +393,7 @@ int tpkg_validate(const tpkg_manifest* m)
397
393
  if (s->size > UINT64_MAX - s->offset) {
398
394
  return tpkg__fail(TPKG_ERR_INVALID);
399
395
  }
400
- if (s->format_id > TPKG_FORMAT_ZIP) {
396
+ if (s->format_id > TPKG_FORMAT_RUNTIME) {
401
397
  return tpkg__fail(TPKG_ERR_INVALID);
402
398
  }
403
399
  if (tpkg__strnlen(s->mount_point, TPKG_MOUNT_POINT_LEN) == TPKG_MOUNT_POINT_LEN) {
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 [Ribose Inc](https://www.ribose.com).
4
+ # All rights reserved.
5
+ # This file is a part of the Tebako project.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions
9
+ # are met:
10
+ # 1. Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # 2. Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
18
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
+ # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
20
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ # POSSIBILITY OF SUCH DAMAGE.
27
+
28
+ require_relative "runtime_manager"
29
+
30
+ # Tebako - an executable packager
31
+ module Tebako
32
+ # Resolution, download, verification and machine-wide caching of the
33
+ # tebako-bootstrap launcher binaries published by tamatebako/tebako-bootstrap.
34
+ # The launcher is the base binary of lean/fat three-part packages.
35
+ #
36
+ # Cache layout (rooted at $TEBAKO_HOME or ~/.tebako, shared with the
37
+ # runtime package cache):
38
+ # bootstraps/tebako-bootstrap-<version>-<platform>/
39
+ # tebako-bootstrap-<version>-<platform>[.exe]
40
+ # sha256 -- digest the installed file was verified against
41
+ # origin -- URL the binary was downloaded from
42
+ #
43
+ # All download/verify/lock machinery is inherited from RuntimeManager; only
44
+ # the release-shape specifics (asset naming, manifest.json object format,
45
+ # SHA256SUMS index name, mirror env var) differ.
46
+ class BootstrapManager < RuntimeManager
47
+ DEFAULT_MIRROR = "https://github.com/tamatebako/tebako-bootstrap/releases/download"
48
+ BOOTSTRAPS_DIR = "bootstraps"
49
+ INDEX_FILES = ["manifest.json", "SHA256SUMS"].freeze
50
+
51
+ # tebako-bootstrap release consumed by default (TEBAKO_BOOTSTRAP_VERSION
52
+ # overrides). 0.2.0 is the first release that installs a fat package's
53
+ # runtime payload slot into the cache at first run.
54
+ BOOTSTRAP_VERSION = "0.2.0"
55
+ PAYLOAD_MIN_VERSION = "0.2.0"
56
+
57
+ class << self
58
+ def default_version
59
+ version = ENV.fetch("TEBAKO_BOOTSTRAP_VERSION", nil)
60
+ version.nil? || version.empty? ? BOOTSTRAP_VERSION : version.sub(/\Av/, "")
61
+ end
62
+
63
+ def resolve(platform, bootstrap_version: default_version)
64
+ new.resolve(platform, bootstrap_version: bootstrap_version)
65
+ end
66
+ end
67
+
68
+ def resolve(platform, bootstrap_version: self.class.default_version)
69
+ super(bootstrap_version, platform, tebako_version: bootstrap_version)
70
+ end
71
+
72
+ private
73
+
74
+ def default_mirror
75
+ DEFAULT_MIRROR
76
+ end
77
+
78
+ def mirror_env_var
79
+ "TEBAKO_BOOTSTRAP_MIRROR"
80
+ end
81
+
82
+ def index_files
83
+ INDEX_FILES
84
+ end
85
+
86
+ def cache_subdir
87
+ BOOTSTRAPS_DIR
88
+ end
89
+
90
+ def release_name
91
+ "tebako-bootstrap"
92
+ end
93
+
94
+ def entry_dir(_version, platform, bootstrap_version)
95
+ File.join(@cache_root, BOOTSTRAPS_DIR, "tebako-bootstrap-#{bootstrap_version}-#{platform}")
96
+ end
97
+
98
+ def runtime_filename(_version, platform, bootstrap_version)
99
+ suffix = platform.start_with?("windows") ? ".exe" : ""
100
+ "tebako-bootstrap-#{bootstrap_version}-#{platform}#{suffix}"
101
+ end
102
+
103
+ def runtime_ref(_version, platform, bootstrap_version)
104
+ "tebako-bootstrap@#{bootstrap_version} (#{platform})"
105
+ end
106
+
107
+ def find_entry(index, _version, platform, bootstrap_version)
108
+ entry = index.find { |candidate| candidate["platform"] == platform }
109
+ return entry if entry
110
+
111
+ platforms = index.map { |candidate| candidate["platform"] }
112
+ Tebako.packaging_error(131, "no tebako-bootstrap package for #{platform} (bootstrap #{bootstrap_version}). " \
113
+ "Available: #{platforms.join(", ")}.")
114
+ end
115
+
116
+ def offline_check(ref, bootstrap_version)
117
+ return unless offline?
118
+
119
+ Tebako.packaging_error(132, "#{ref} is not cached and downloads are disabled " \
120
+ "(release index: #{index_urls(bootstrap_version).join(", ")}; " \
121
+ "#{mirror_env_var}=#{@mirror})")
122
+ end
123
+
124
+ # tebako-bootstrap's manifest.json is an object
125
+ # ({name:, version:, assets: [{platform:, file:, sha256:}]}), not the
126
+ # entry array tebako-runtime-ruby publishes
127
+ def parse_manifest(body, _bootstrap_version)
128
+ manifest_assets(body)
129
+ .select { |asset| asset["platform"] && asset["file"] && asset["sha256"] }
130
+ .map { |asset| manifest_entry(asset) }
131
+ end
132
+
133
+ def manifest_assets(body)
134
+ data = JSON.parse(body)
135
+ assets = data.is_a?(Hash) ? data["assets"] : nil
136
+ raise IndexUnavailable, "manifest.json has no assets array" unless assets.is_a?(Array)
137
+
138
+ assets
139
+ rescue JSON::ParserError
140
+ raise IndexUnavailable, "manifest.json is not valid JSON"
141
+ end
142
+
143
+ def manifest_entry(asset)
144
+ { "platform" => asset["platform"], "filename" => asset["file"], "sha256" => asset["sha256"].downcase }
145
+ end
146
+
147
+ def parse_sha256sums(body, bootstrap_version)
148
+ pattern = /\Atebako-bootstrap-#{Regexp.escape(bootstrap_version)}-(.+?)(?:\.exe)?\z/
149
+ body.each_line.filter_map do |line|
150
+ sha256, file = line.strip.split(/\s+/, 2)
151
+ match = file && pattern.match(file.sub(/\A\*/, ""))
152
+ next unless match
153
+
154
+ { "platform" => match[1], "filename" => file.sub(/\A\*/, ""), "sha256" => sha256.downcase }
155
+ end
156
+ end
157
+ end
158
+ end
data/lib/tebako/cli.rb CHANGED
@@ -174,9 +174,10 @@ module Tebako
174
174
  DESC
175
175
 
176
176
  RUNTIME_DESCRIPTION = <<~DESC
177
- Runtime provenance: 'prebuilt' resolves/downloads a prebuilt tebako runtime package and stitches the
178
- #{" " * 65}# application image onto it (default for the 'bundle' mode); 'source' keeps the Stage-2 source build.
179
- #{" " * 65}# Modes other than 'bundle' always build from source.
177
+ Runtime provenance: 'prebuilt' resolves/downloads a prebuilt tebako runtime package (default for the
178
+ #{" " * 65}# 'bundle', 'classic', 'lean' and 'fat' modes); 'source' keeps the Stage-2 source build
179
+ #{" " * 65}# (not available for 'lean'/'fat' -- the bootstrap resolves tebako-runtime-ruby releases
180
+ #{" " * 65}# at run time). Modes other than those always build from source.
180
181
  DESC
181
182
 
182
183
  IMAGE_DESCRIPTION = <<~DESC
@@ -184,6 +185,16 @@ module Tebako
184
185
  #{" " * 65}# must be distinct. Prebuilt runtime only.
185
186
  DESC
186
187
 
188
+ MODE_DESCRIPTION = <<~DESC
189
+ Tebako press mode, 'lean' by default.
190
+ #{" " * 65}# 'lean' presses a three-part package (tebako-bootstrap + application image(s) + tpkg trailer);
191
+ #{" " * 65}# the runtime is resolved into the shared cache at first run.
192
+ #{" " * 65}# 'fat' is 'lean' plus the runtime package as a payload slot -- the first run installs it
193
+ #{" " * 65}# into the cache without network access.
194
+ #{" " * 65}# 'classic' stitches the application image onto a prebuilt runtime (Stage-3A layout);
195
+ #{" " * 65}# 'bundle', 'both', 'application' and 'runtime' keep their legacy behaviors.
196
+ DESC
197
+
187
198
  desc "press", "Press tebako image"
188
199
  method_option :cwd, type: :string, aliases: "-c", required: false, desc: CWD_DESCRIPTION
189
200
  method_option :"log-level", type: :string, aliases: "-l", required: false, enum: %w[error warn debug trace],
@@ -197,8 +208,9 @@ module Tebako
197
208
  enum: Tebako::RubyVersion::RUBY_VERSIONS.keys,
198
209
  desc: "Tebako package Ruby version, #{Tebako::RubyVersion::DEFAULT_RUBY_VERSION} by default"
199
210
  method_option :patchelf, aliases: "-P", type: :boolean, desc: RGP_DESCRIPTION
200
- method_option :mode, type: :string, aliases: "-m", required: false, enum: %w[bundle both runtime application],
201
- desc: "Tebako press mode, 'bundle' by default"
211
+ method_option :mode, type: :string, aliases: "-m", required: false,
212
+ enum: %w[lean fat classic bundle both runtime application],
213
+ desc: MODE_DESCRIPTION
202
214
  method_option :ref, type: :string, aliases: "-u", required: false, desc: REF_DESCRIPTION
203
215
  method_option :runtime, type: :string, required: false, enum: %w[prebuilt source], desc: RUNTIME_DESCRIPTION
204
216
  method_option :"build-runtime", type: :boolean, required: false,
@@ -25,13 +25,16 @@
25
25
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
26
  # POSSIBILITY OF SUCH DAMAGE.
27
27
 
28
+ require "digest"
28
29
  require "etc"
29
30
  require "fileutils"
30
31
  require "pathname"
31
32
  require "rbconfig"
32
33
 
34
+ require_relative "bootstrap_manager"
33
35
  require_relative "codegen"
34
36
  require_relative "error"
37
+ require_relative "launcher_abi"
35
38
  require_relative "options_manager"
36
39
  require_relative "runtime_manager"
37
40
  require_relative "scenario_manager"
@@ -86,8 +89,13 @@ module Tebako
86
89
  options_manager.process_gemfile(scenario_manager.gemfile_path) if scenario_manager.with_gemfile
87
90
  check_warnings(options_manager)
88
91
  puts options_manager.press_announce(scenario_manager.msys?)
92
+ dispatch_press(options_manager, scenario_manager)
93
+ end
89
94
 
90
- if options_manager.prebuilt_runtime?
95
+ def dispatch_press(options_manager, scenario_manager)
96
+ if options_manager.three_part?
97
+ do_press_three_part(options_manager, scenario_manager)
98
+ elsif options_manager.prebuilt_runtime?
91
99
  do_press_prebuilt(options_manager, scenario_manager)
92
100
  else
93
101
  do_press_source(options_manager, scenario_manager)
@@ -104,7 +112,8 @@ module Tebako
104
112
  def do_press_prebuilt(options_manager, scenario_manager)
105
113
  Tebako::Packager.check_prebuilt_env!(options_manager.stash_dir, options_manager.deps_bin_dir)
106
114
  runtime_path = Tebako::RuntimeManager.resolve(options_manager.ruby_ver, options_manager.host_platform)
107
- app_image = Tebako::Packager.build_app_image(options_manager, scenario_manager)
115
+ app_image = Tebako::Packager.build_app_image(options_manager, scenario_manager,
116
+ Tebako::RuntimeManager.layout(runtime_path))
108
117
 
109
118
  images = [{ path: app_image, mount_point: scenario_manager.fs_mount_point,
110
119
  format_id: Tebako::Stitcher::FORMAT_DWARFS }] + options_manager.images
@@ -113,6 +122,72 @@ module Tebako
113
122
  puts "Created tebako #{options_manager.output_type_first} at \"#{package}\""
114
123
  end
115
124
 
125
+ # Press a three-part package (Stage 3B): tebako-bootstrap + application
126
+ # image slot(s) + tpkg trailer with the runtime reference (launcher ABI
127
+ # v1). 'lean' (the default) resolves the runtime into the shared cache at
128
+ # first run; 'fat' additionally embeds the runtime package as a payload
129
+ # slot, so the first run installs it without any network access.
130
+ def do_press_three_part(options_manager, scenario_manager)
131
+ bootstrap_path, runtime_path = resolve_three_part_parts(options_manager)
132
+ app_image = Tebako::Packager.build_app_image(options_manager, scenario_manager,
133
+ Tebako::RuntimeManager.layout(runtime_path))
134
+ payload_path = options_manager.fat? ? runtime_path : nil
135
+ images = three_part_images(options_manager, scenario_manager, app_image, payload_path)
136
+ package = "#{options_manager.package}#{scenario_manager.exe_suffix}"
137
+ stitch_three_part(options_manager, bootstrap_path, images, package, payload_path)
138
+ puts "Created tebako #{options_manager.output_type_first} at \"#{package}\""
139
+ end
140
+
141
+ def stitch_three_part(options_manager, bootstrap_path, images, package, payload_path)
142
+ Tebako::Stitcher.stitch(bootstrap_path, images: images, output: package, lean: true,
143
+ ruby_version: options_manager.ruby_ver,
144
+ launcher_abi: Tebako::LauncherAbi::VERSION,
145
+ runtime_sha256: payload_sha256(payload_path))
146
+ end
147
+
148
+ def payload_sha256(payload_path)
149
+ payload_path && Digest::SHA256.file(payload_path).hexdigest
150
+ end
151
+
152
+ # Validate the options and the packaging environment, then resolve the
153
+ # bootstrap and the runtime into the shared cache. The runtime is needed
154
+ # in both three-part modes: 'fat' embeds it as a payload slot, 'lean'
155
+ # uses its extracted layout to align the application image's arch
156
+ # conventions (and references it in the trailer for first-run resolution)
157
+ def resolve_three_part_parts(options_manager)
158
+ options_manager.runtime_source # validates the mode/provenance combination
159
+ check_bootstrap_version!(options_manager)
160
+ Tebako::Packager.check_prebuilt_env!(options_manager.stash_dir, options_manager.deps_bin_dir)
161
+ runtime_path = Tebako::RuntimeManager.resolve(options_manager.ruby_ver, options_manager.host_platform)
162
+ [Tebako::BootstrapManager.resolve(options_manager.host_platform), runtime_path]
163
+ end
164
+
165
+ def three_part_images(options_manager, scenario_manager, app_image, runtime_path)
166
+ images = [{ path: app_image, mount_point: scenario_manager.fs_mount_point,
167
+ format_id: Tebako::Stitcher::FORMAT_DWARFS }] + options_manager.images
168
+ images << { path: runtime_path, mount_point: "", format_id: Tebako::Stitcher::FORMAT_RUNTIME } if runtime_path
169
+ images
170
+ end
171
+
172
+ # The fat payload slot is installed by the bootstrap at first run — a
173
+ # capability added in tebako-bootstrap 0.2.0
174
+ def check_bootstrap_version!(options_manager)
175
+ return unless options_manager.fat?
176
+
177
+ version = Tebako::BootstrapManager.default_version
178
+ minimum = Tebako::BootstrapManager::PAYLOAD_MIN_VERSION
179
+ return if payload_capable?(version, minimum)
180
+
181
+ Tebako.packaging_error(134, "fat mode requires tebako-bootstrap >= #{minimum} (selected: #{version}; " \
182
+ "set TEBAKO_BOOTSTRAP_VERSION to a payload-capable release)")
183
+ end
184
+
185
+ def payload_capable?(version, minimum)
186
+ Gem::Version.new(version) >= Gem::Version.new(minimum)
187
+ rescue ArgumentError
188
+ false
189
+ end
190
+
116
191
  def do_press_application(options_manager, scenario_manager)
117
192
  return unless %w[both application].include?(options_manager.mode)
118
193
 
@@ -121,7 +196,7 @@ module Tebako
121
196
  end
122
197
 
123
198
  def do_press_runtime(options_manager, scenario_manager)
124
- return unless %w[both runtime bundle].include?(options_manager.mode)
199
+ return unless %w[both runtime bundle classic].include?(options_manager.mode)
125
200
 
126
201
  generate_files(options_manager, scenario_manager)
127
202
  merged_env = ENV.to_h.merge(scenario_manager.b_env)
@@ -190,7 +190,11 @@ module Tebako
190
190
  def configure_commands_not_msys
191
191
  @cmd_suffix = ""
192
192
  @bat_suffix = ""
193
- @force_ruby_platform = "false"
193
+ # Force the ruby (source) platform for gems: precompiled variants link
194
+ # against shared system libraries (libffi & co.) that do not exist
195
+ # inside a tebako package -- e.g. ffi-x86_64-linux-gnu fails to load in
196
+ # the memfs (tebako#343). msys has always forced this.
197
+ @force_ruby_platform = "true"
194
198
  @nokogiri_option = "--no-use-system-libraries"
195
199
  end
196
200
 
data/lib/tebako/error.rb CHANGED
@@ -57,6 +57,9 @@ module Tebako
57
57
  127 => "Stitch input file is not accessible",
58
58
  128 => "Prebuilt runtime press requires the packaging environment (run 'tebako setup' first)",
59
59
  130 => "Option combination is not supported",
60
+ 131 => "No tebako-bootstrap package for the requested platform",
61
+ 132 => "TEBAKO_OFFLINE is set and the requested tebako-bootstrap package is not cached",
62
+ 134 => "Fat mode requires a payload-capable tebako-bootstrap release",
60
63
  201 => "Warning. Could not create cache version file"
61
64
  }.freeze
62
65
 
@@ -30,6 +30,12 @@ module Tebako
30
30
  # three-part package model (spec §4.4).
31
31
  #
32
32
  # A lean package is [tebako-bootstrap][.tfs image slots][tpkg trailer].
33
+ # A fat package has the same layout plus the runtime package itself as a
34
+ # payload slot (format_id TPKG_FORMAT_RUNTIME, empty mount point,
35
+ # ";sha256=<hex>" appended to the trailer's runtime_ref): at first run the
36
+ # bootstrap installs the payload into the shared cache instead of
37
+ # downloading it. Payload slots are never handed over as --tebako-image.
38
+ #
33
39
  # The bootstrap (tamatebako/tebako-bootstrap) parses the trailer of its own
34
40
  # executable, resolves the language runtime into the shared cache, and
35
41
  # execs it as:
@@ -41,6 +41,13 @@ require_relative "version"
41
41
  module Tebako
42
42
  # Cli helpers
43
43
  class OptionsManager # rubocop:disable Metrics/ClassLength
44
+ # Press modes producing three-part packages (tebako-bootstrap + image
45
+ # slots + tpkg trailer): 'lean' resolves the runtime at first run, 'fat'
46
+ # additionally embeds it as a payload slot (self-installing, offline)
47
+ THREE_PART_MODES = %w[lean fat].freeze
48
+ # Modes whose default runtime provenance is a prebuilt runtime package
49
+ PREBUILT_DEFAULT_MODES = %w[bundle classic lean fat].freeze
50
+
44
51
  def initialize(options)
45
52
  @options = options
46
53
  @rv = Tebako::RubyVersion.new(@options["Ruby"])
@@ -157,14 +164,24 @@ module Tebako
157
164
  end
158
165
 
159
166
  def mode
160
- @mode ||= @options["mode"].nil? ? "bundle" : @options["mode"]
167
+ @mode ||= @options["mode"].nil? ? "lean" : @options["mode"]
168
+ end
169
+
170
+ def three_part?
171
+ THREE_PART_MODES.include?(mode)
172
+ end
173
+
174
+ def fat?
175
+ mode == "fat"
161
176
  end
162
177
 
163
178
  # Runtime provenance: "prebuilt" (resolve/download a prebuilt runtime
164
- # package and stitch the application image onto it) or "source" (the
165
- # Stage-2 source build). Default is "prebuilt" for the bundle mode;
166
- # other modes always build from source, and --build-runtime forces the
167
- # source path everywhere (back-compat alias for '--runtime source').
179
+ # package) or "source" (the Stage-2 source build). Default is "prebuilt"
180
+ # for the bundle/classic/lean/fat modes; other modes always build from
181
+ # source, and --build-runtime forces the source path everywhere it is
182
+ # allowed (back-compat alias for '--runtime source'). The three-part
183
+ # modes (lean/fat) require prebuilt runtime packages: the bootstrap
184
+ # resolves tebako-runtime-ruby releases at run time.
168
185
  def runtime_source
169
186
  @runtime_source ||= checked_runtime_source(requested_runtime_source)
170
187
  end
@@ -246,7 +263,7 @@ module Tebako
246
263
  press_announce_application(is_msys)
247
264
  when "both"
248
265
  press_announce_both
249
- when "bundle"
266
+ when "bundle", "classic", "lean", "fat"
250
267
  press_announce_bundle
251
268
  when "runtime"
252
269
  press_announce_runtime
@@ -290,7 +307,7 @@ module Tebako
290
307
  def press_announce_bundle
291
308
  <<~ANN
292
309
  Running tebako press at #{prefix}
293
- Mode: 'bundle'
310
+ Mode: '#{mode}'
294
311
  Ruby version: '#{@ruby_ver}'
295
312
  Project root: '#{root}'
296
313
  Application entry point: '#{fs_entrance}'
@@ -374,14 +391,19 @@ module Tebako
374
391
  def requested_runtime_source
375
392
  explicit = @options["runtime"]
376
393
  return "source" if @options["build-runtime"]
377
- return mode == "bundle" ? "prebuilt" : "source" if explicit.nil?
394
+ return PREBUILT_DEFAULT_MODES.include?(mode) ? "prebuilt" : "source" if explicit.nil?
378
395
 
379
396
  explicit
380
397
  end
381
398
 
382
399
  def checked_runtime_source(source)
383
- if source == "prebuilt" && mode != "bundle"
384
- Tebako.packaging_error(130, "--runtime prebuilt is supported in bundle mode only (mode: '#{mode}')")
400
+ if source == "prebuilt" && !PREBUILT_DEFAULT_MODES.include?(mode)
401
+ Tebako.packaging_error(130, "--runtime prebuilt is supported in bundle, classic, lean and fat modes only " \
402
+ "(mode: '#{mode}')")
403
+ end
404
+ if source == "source" && three_part?
405
+ Tebako.packaging_error(130, "mode '#{mode}' requires a prebuilt runtime package " \
406
+ "(the bootstrap resolves tebako-runtime-ruby releases at first run)")
385
407
  end
386
408
  source
387
409
  end
@@ -399,7 +421,7 @@ module Tebako
399
421
 
400
422
  def host_arch_id(arch)
401
423
  case arch
402
- when /^(x86_64|amd64)$/ then "x86_64"
424
+ when /^(x86_64|amd64|x64)$/ then "x86_64"
403
425
  when /^(aarch64|arm64)$/ then "arm64"
404
426
  else
405
427
  Tebako.packaging_error(112, arch)
@@ -203,6 +203,21 @@ module Tebako
203
203
  "/* tebako patched */ static const struct vktable *console_win32_vk(const char *, size_t);"
204
204
  }.freeze
205
205
 
206
+ # Ruby's win32.c defines clock_gettime/clock_getres fallbacks with no
207
+ # HAVE_* configure guard (at least through 3.4); modern mingw-w64
208
+ # winpthreads headers (pthread_time.h, pulled in by time.h) carry
209
+ # static-inline definitions of the same symbols, so the toolchain
210
+ # build fails with redefinition errors. Rename ruby's fallbacks
211
+ # before the toolchain build; the CRT/winpthreads implementations
212
+ # (which configure detects) serve the calls. Pass2 re-applies the
213
+ # same rename from pristine sources for the final build.
214
+ WIN32_WIN32_C_CLOCK_PATCH = {
215
+ "clock_gettime(clockid_t clock_id, struct timespec *sp)" =>
216
+ "_dummy_clock_gettime(clockid_t clock_id, struct timespec *sp)",
217
+ "clock_getres(clockid_t clock_id, struct timespec *sp)" =>
218
+ "_dummy_clock_getres(clockid_t clock_id, struct timespec *sp)"
219
+ }.freeze
220
+
206
221
  def patch_map
207
222
  pm = msys_patches
208
223
  pm.merge!(super)
@@ -246,7 +261,11 @@ module Tebako
246
261
  "ext/openssl/extconf.rb" => OPENSSL_EXTCONF_RB_PATCH,
247
262
  # ....................................................
248
263
  # Fix the signature of console_win32_vk
249
- "ext/io/console/win32_vk.inc" => EXT_IO_CONSOLE_WIN32_VK_INC_PATCH
264
+ "ext/io/console/win32_vk.inc" => EXT_IO_CONSOLE_WIN32_VK_INC_PATCH,
265
+ # ....................................................
266
+ # Rename ruby's clock_gettime/clock_getres fallbacks before the
267
+ # toolchain build (winpthreads static-inline collision)
268
+ "win32/win32.c" => WIN32_WIN32_C_CLOCK_PATCH
250
269
  }
251
270
  end
252
271
 
@@ -114,6 +114,9 @@ module Tebako
114
114
  "#if defined _MSC_VER && _MSC_VER <= 1200" =>
115
115
  "#{PatchLiterals::C_FILE_SUBST_LESS}\n#if defined _MSC_VER && _MSC_VER <= 1200",
116
116
  "len = GetCurrentDirectoryW(0, NULL);" => WIN32_WIN32_C_MSYS_SUBST,
117
+ # Keep the clock renames in sync with Pass1MSysPatch::WIN32_WIN32_C_CLOCK_PATCH:
118
+ # pass1 applies them for the toolchain build, this pass re-applies them
119
+ # from pristine sources for the final build
117
120
  "clock_gettime(clockid_t clock_id, struct timespec *sp)" =>
118
121
  "_dummy_clock_gettime(clockid_t clock_id, struct timespec *sp)",
119
122
  "clock_getres(clockid_t clock_id, struct timespec *sp)" =>
@@ -76,7 +76,9 @@ module Tebako
76
76
 
77
77
  def recreate(dirname)
78
78
  FileUtils.rm_rf(dirname, noop: nil, verbose: nil, secure: true)
79
- FileUtils.mkdir(dirname)
79
+ # mkdir_p: in three-part/prebuilt press flows no cmake configure runs,
80
+ # so the parent output folder may not exist yet
81
+ FileUtils.mkdir_p(dirname)
80
82
  end
81
83
 
82
84
  def restore_and_save(fname, strict: true)
@@ -129,16 +129,72 @@ module Tebako
129
129
  # Deploy the application and build its DwarFS image for stitching onto
130
130
  # a prebuilt runtime. Same layout as the bundle-mode image, plus an
131
131
  # entry dispatcher at /local/stub.rb (the runtime's compiled-in entry).
132
- def build_app_image(options_manager, scenario_manager) # rubocop:disable Metrics/AbcSize
132
+ # When layout_dir is given (the resolved runtime's extracted layout),
133
+ # the image's arch conventions are aligned to the runtime's: ruby's
134
+ # compiled-in search paths come from the runtime build, while the
135
+ # local packaging environment names arch directories after the press
136
+ # machine -- on macOS the arch string embeds the kernel version
137
+ # (arm64-darwin23 vs arm64-darwin24), so they differ whenever press
138
+ # and runtime were built on different macOS releases.
139
+ def build_app_image(options_manager, scenario_manager, layout_dir = nil) # rubocop:disable Metrics/AbcSize
133
140
  init(options_manager.stash_dir, options_manager.data_src_dir, options_manager.data_pre_dir,
134
141
  options_manager.data_bin_dir)
135
142
  deploy(options_manager.data_src_dir, options_manager.data_pre_dir, options_manager.rv,
136
143
  options_manager.root, scenario_manager.fs_entrance, options_manager.cwd)
144
+ align_layout_to_runtime!(options_manager.data_src_dir, layout_dir, options_manager.rv) if layout_dir
137
145
  write_entry_dispatcher(options_manager.data_src_dir, scenario_manager, options_manager.cwd)
138
146
  mkdwarfs(options_manager.deps_bin_dir, options_manager.data_bundle_file, options_manager.data_src_dir)
139
147
  options_manager.data_bundle_file
140
148
  end
141
149
 
150
+ # Rename the image's arch directories to the runtime's names and drop
151
+ # in the runtime's own rbconfig.rb. No-op when the conventions already
152
+ # match (always the case off macOS, where the arch string carries no
153
+ # OS version).
154
+ def align_layout_to_runtime!(data_src_dir, layout_dir, ruby_ver)
155
+ align_stdlib_arch!(data_src_dir, layout_dir, ruby_ver.api_version)
156
+ align_gem_ext_arch!(data_src_dir, layout_dir, ruby_ver.api_version)
157
+ end
158
+
159
+ def align_stdlib_arch!(data_src_dir, layout_dir, api_ver)
160
+ runtime_arch = arch_dir_of(File.join(layout_dir, "lib", "ruby", api_ver), "rbconfig.rb")
161
+ image_arch = arch_dir_of(File.join(data_src_dir, "lib", "ruby", api_ver), "rbconfig.rb")
162
+ return if runtime_arch.nil? || image_arch.nil? || runtime_arch == image_arch
163
+
164
+ puts " ... aligning app image layout to the runtime (#{image_arch} -> #{runtime_arch})"
165
+ FileUtils.mv(File.join(data_src_dir, "lib", "ruby", api_ver, image_arch),
166
+ File.join(data_src_dir, "lib", "ruby", api_ver, runtime_arch))
167
+ FileUtils.cp(File.join(layout_dir, "lib", "ruby", api_ver, runtime_arch, "rbconfig.rb"),
168
+ File.join(data_src_dir, "lib", "ruby", api_ver, runtime_arch, "rbconfig.rb"))
169
+ end
170
+
171
+ # Native-gem extensions dir uses the dashed flavor of the arch string
172
+ # (arm64-darwin-23); align it to the runtime's as well
173
+ def align_gem_ext_arch!(data_src_dir, layout_dir, api_ver)
174
+ img_ext = File.join(data_src_dir, "lib", "ruby", "gems", api_ver, "extensions")
175
+ rt_ext = File.join(layout_dir, "lib", "ruby", "gems", api_ver, "extensions")
176
+ runtime_ext = first_dir(rt_ext)
177
+ return if runtime_ext.nil? || !Dir.exist?(img_ext)
178
+
179
+ Dir.children(img_ext).each do |d|
180
+ next if d == runtime_ext || !File.directory?(File.join(img_ext, d))
181
+
182
+ FileUtils.mv(File.join(img_ext, d), File.join(img_ext, runtime_ext))
183
+ end
184
+ end
185
+
186
+ def arch_dir_of(dir, marker)
187
+ return nil unless Dir.exist?(dir)
188
+
189
+ Dir.children(dir).find { |d| File.exist?(File.join(dir, d, marker)) }
190
+ end
191
+
192
+ def first_dir(dir)
193
+ return nil unless Dir.exist?(dir)
194
+
195
+ Dir.children(dir).find { |d| File.directory?(File.join(dir, d)) }
196
+ end
197
+
142
198
  # The prebuilt runtime packages are pressed in 'runtime' mode, so their
143
199
  # compiled-in entry point is /local/stub.rb. A stitched package mounts
144
200
  # the application image as the root filesystem; the dispatcher written
@@ -46,7 +46,14 @@ module Tebako
46
46
  "3.3.6" => "8dc48fffaf270f86f1019053f28e51e4da4cce32a36760a0603a9aee67d7fd8d",
47
47
  "3.3.7" => "9c37c3b12288c7aec20ca121ce76845be5bb5d77662a24919651aaf1d12c8628",
48
48
  "3.4.1" => "3d385e5d22d368b064c817a13ed8e3cc3f71a7705d7ed1bae78013c33aa7c87f",
49
- "3.4.2" => "41328ac21f2bfdd7de6b3565ef4f0dd7543354d37e96f157a1552a6bd0eb364b"
49
+ "3.4.2" => "41328ac21f2bfdd7de6b3565ef4f0dd7543354d37e96f157a1552a6bd0eb364b",
50
+ "4.0.0" => "2e8389c8c072cb658c93a1372732d9eac84082c88b065750db1e52a5ac630271",
51
+ "4.0.1" => "3924be2d05db30f4e35f859bf028be85f4b7dd01714142fd823e4af5de2faf9d",
52
+ "4.0.2" => "51502b26b50b68df4963336ca41e368cde92c928faf91654de4c4c1791f82aac",
53
+ "4.0.3" => "77964acc370d5c8375b9502e5ba6c13c03ef91ab9eb9f521c84fb42b9c9a6b0f",
54
+ "4.0.4" => "f35f6edfa3dabb3f723f9d0cf1906c6512ae77f4e412ab1e68cc6e91d230fa80",
55
+ "4.0.5" => "7d6149079a63f8ae1d326c9fa65c6019ba2dc3155eae7b39159817911c88958e",
56
+ "4.0.6" => "837d299e8f7ddf2be31a229a7a7e019d354979825117989acb3b32b1a9be262a"
50
57
  }.freeze
51
58
 
52
59
  MIN_RUBY_VERSION_WINDOWS = "3.1.6"
@@ -72,38 +79,40 @@ module Tebako
72
79
  @lib_version ||= "#{@ruby_version.split(".")[0..1].join}0"
73
80
  end
74
81
 
82
+ # Version gates compare numerically so 4.x lines fall out naturally
83
+ # (string indexing broke the moment the major version hit 4)
75
84
  def ruby3x?
76
- @ruby3x ||= @ruby_version[0] == "3"
85
+ @ruby3x ||= version_at_least?(3, 0)
77
86
  end
78
87
 
79
88
  def ruby31?
80
- @ruby31 ||= ruby3x? && @ruby_version[2].to_i >= 1
89
+ @ruby31 ||= version_at_least?(3, 1)
81
90
  end
82
91
 
83
92
  def ruby32?
84
- @ruby32 ||= ruby3x? && @ruby_version[2].to_i >= 2
93
+ @ruby32 ||= version_at_least?(3, 2)
85
94
  end
86
95
 
87
96
  def ruby32only?
88
- @ruby32only ||= ruby3x? && @ruby_version[2] == "2"
97
+ @ruby32only ||= major_minor == [3, 2]
89
98
  end
90
99
 
91
100
  def ruby33?
92
- @ruby33 ||= ruby3x? && @ruby_version[2].to_i >= 3
101
+ @ruby33 ||= version_at_least?(3, 3)
93
102
  end
94
103
 
95
104
  def ruby33only?
96
- @ruby33only ||= ruby3x? && @ruby_version[2] == "3"
105
+ @ruby33only ||= major_minor == [3, 3]
97
106
  end
98
107
 
99
108
  def ruby3x7?
100
109
  @ruby3x7 ||= ruby34? ||
101
- (ruby33only? && @ruby_version[4].to_i >= 7) ||
102
- (ruby32only? && @ruby_version[4].to_i >= 7)
110
+ (ruby33only? && patch_version >= 7) ||
111
+ (ruby32only? && patch_version >= 7)
103
112
  end
104
113
 
105
114
  def ruby34?
106
- @ruby34 ||= ruby3x? && @ruby_version[2].to_i >= 4
115
+ @ruby34 ||= version_at_least?(3, 4)
107
116
  end
108
117
 
109
118
  def run_checks
@@ -112,6 +121,22 @@ module Tebako
112
121
  version_check_msys
113
122
  end
114
123
 
124
+ private
125
+
126
+ def major_minor
127
+ @major_minor ||= @ruby_version.split(".").first(2).map(&:to_i)
128
+ end
129
+
130
+ def patch_version
131
+ @patch_version ||= @ruby_version.split(".")[2].to_i
132
+ end
133
+
134
+ def version_at_least?(major, minor)
135
+ (major_minor <=> [major, minor]) >= 0
136
+ end
137
+
138
+ public
139
+
115
140
  def version_check
116
141
  return if RUBY_VERSIONS.key?(@ruby_version)
117
142
 
@@ -31,6 +31,7 @@ require "json"
31
31
  require "net/http"
32
32
  require "uri"
33
33
 
34
+ require_relative "build_helpers"
34
35
  require_relative "error"
35
36
  require_relative "version"
36
37
 
@@ -83,11 +84,15 @@ module Tebako
83
84
  def resolve(ruby_version, platform, tebako_version: Tebako::VERSION)
84
85
  new.resolve(ruby_version, platform, tebako_version: tebako_version)
85
86
  end
87
+
88
+ def layout(runtime_path)
89
+ new.layout(runtime_path)
90
+ end
86
91
  end
87
92
 
88
93
  def initialize(cache_root: nil, mirror: nil, lock_timeout: LOCK_TIMEOUT, retry_delay: RETRY_DELAY)
89
94
  @cache_root = cache_root || self.class.default_cache_root
90
- @mirror = (mirror || ENV.fetch("TEBAKO_RUNTIME_MIRROR", nil) || DEFAULT_MIRROR).sub(%r{/+\z}, "")
95
+ @mirror = (mirror || ENV.fetch(mirror_env_var, nil) || default_mirror).sub(%r{/+\z}, "")
91
96
  @lock_timeout = lock_timeout
92
97
  @retry_delay = retry_delay
93
98
  end
@@ -105,8 +110,24 @@ module Tebako
105
110
  executable
106
111
  end
107
112
 
113
+ # Extract the runtime package's filesystem layout next to the cached
114
+ # package (idempotent) and return the layout root. The layout is the
115
+ # authoritative source of the runtime's arch conventions (rbconfig
116
+ # location, gem extension dir naming), which the application image must
117
+ # match: ruby's compiled-in search paths come from the runtime build,
118
+ # while a local packaging environment names them after the press machine
119
+ # (macOS embeds the kernel version, e.g. arm64-darwin23 vs -darwin24).
120
+ def layout(runtime_path)
121
+ layout_dir = File.join(File.dirname(runtime_path), "layout")
122
+ return layout_dir if File.directory?(File.join(layout_dir, "lib"))
123
+
124
+ FileUtils.mkdir_p(layout_dir)
125
+ Tebako::BuildHelpers.run_with_capture_v([runtime_path, "--tebako-extract", layout_dir])
126
+ layout_dir
127
+ end
128
+
108
129
  def entries
109
- base = File.join(@cache_root, RUNTIMES_DIR)
130
+ base = File.join(@cache_root, cache_subdir)
110
131
  return [] unless Dir.exist?(base)
111
132
 
112
133
  Dir.children(base).sort.filter_map do |name|
@@ -131,6 +152,29 @@ module Tebako
131
152
 
132
153
  private
133
154
 
155
+ # ---- extension points for BootstrapManager (tebako-bootstrap release
156
+ # resolution); overridden there, everything else is shared machinery ----
157
+
158
+ def default_mirror
159
+ DEFAULT_MIRROR
160
+ end
161
+
162
+ def mirror_env_var
163
+ "TEBAKO_RUNTIME_MIRROR"
164
+ end
165
+
166
+ def index_files
167
+ INDEX_FILES
168
+ end
169
+
170
+ def cache_subdir
171
+ RUNTIMES_DIR
172
+ end
173
+
174
+ def release_name
175
+ "tebako-runtime-ruby"
176
+ end
177
+
134
178
  def install(executable, ruby_version, platform, tebako_version)
135
179
  ref = runtime_ref(ruby_version, platform, tebako_version)
136
180
  offline_check(ref, tebako_version)
@@ -146,7 +190,7 @@ module Tebako
146
190
 
147
191
  Tebako.packaging_error(123, "#{ref} is not cached and downloads are disabled " \
148
192
  "(release index: #{index_urls(tebako_version).join(", ")}; " \
149
- "TEBAKO_RUNTIME_MIRROR=#{@mirror})")
193
+ "#{mirror_env_var}=#{@mirror})")
150
194
  end
151
195
 
152
196
  def find_entry(index, ruby_version, platform, tebako_version)
@@ -178,14 +222,14 @@ module Tebako
178
222
 
179
223
  def fetch_index(tebako_version)
180
224
  tried = []
181
- INDEX_FILES.each do |name|
225
+ index_files.each do |name|
182
226
  return parse_index(name, fetch_text(index_url(name, tebako_version)), tebako_version)
183
227
  rescue IndexUnavailable
184
228
  tried << index_url(name, tebako_version)
185
229
  rescue DownloadFailed => e
186
230
  Tebako.packaging_error(122, e.message)
187
231
  end
188
- Tebako.packaging_error(124, "tebako-runtime-ruby release v#{tebako_version} provides no usable package " \
232
+ Tebako.packaging_error(124, "#{release_name} release v#{tebako_version} provides no usable package " \
189
233
  "index (tried: #{tried.join(", ")})")
190
234
  end
191
235
 
@@ -333,7 +377,7 @@ module Tebako
333
377
  end
334
378
 
335
379
  def index_urls(tebako_version)
336
- INDEX_FILES.map { |name| index_url(name, tebako_version) }
380
+ index_files.map { |name| index_url(name, tebako_version) }
337
381
  end
338
382
 
339
383
  def package_url(filename, tebako_version)
@@ -75,7 +75,10 @@ module Tebako
75
75
  FORMAT_DWARFS = 1
76
76
  FORMAT_SQUASHFS = 2
77
77
  FORMAT_ZIP = 3
78
- FORMAT_IDS = (FORMAT_AUTO..FORMAT_ZIP).freeze
78
+ # Runtime payload slot of a fat package: installed into the shared cache
79
+ # by the bootstrap at first run, never mounted (tpkg.h TPKG_FORMAT_RUNTIME)
80
+ FORMAT_RUNTIME = 4
81
+ FORMAT_IDS = (FORMAT_AUTO..FORMAT_RUNTIME).freeze
79
82
 
80
83
  # Header field offsets
81
84
  OFF_PACKAGE_FLAGS = 14
@@ -89,17 +92,21 @@ module Tebako
89
92
  # copy of the runtime at +runtime_path+, writing +output+.
90
93
  # lean: true marks the package LEAN and records runtime_ref
91
94
  # ("ruby@<ruby_version>;tebako=<tebako_version>"); classic packages
92
- # carry an empty runtime_ref.
95
+ # carry an empty runtime_ref. runtime_sha256 appends ";sha256=<hex>" to
96
+ # the runtime_ref — the checksum the bootstrap verifies a fat package's
97
+ # runtime payload slot against before installing it into the cache.
93
98
  def stitch(runtime_path, images:, output:, lean: false, ruby_version: nil, # rubocop:disable Metrics/ParameterLists
94
- tebako_version: Tebako::VERSION, launcher_abi: 0)
99
+ tebako_version: Tebako::VERSION, launcher_abi: 0, runtime_sha256: nil)
95
100
  images = normalize_images(images)
96
101
  validate_inputs!(runtime_path, images, lean, ruby_version)
102
+ validate_runtime_sha256!(runtime_sha256)
97
103
 
98
104
  FileUtils.mkdir_p(File.dirname(output))
99
105
  FileUtils.cp(runtime_path, output)
100
106
  FileUtils.chmod(0o755, output)
101
107
 
102
- append(images, output, package_flags(lean), runtime_ref(lean, ruby_version, tebako_version), launcher_abi)
108
+ append(images, output, package_flags(lean), runtime_ref(lean, ruby_version, tebako_version, runtime_sha256),
109
+ launcher_abi)
103
110
  resign_if_needed(output)
104
111
  output
105
112
  end
@@ -157,6 +164,8 @@ module Tebako
157
164
  seen = {}
158
165
  images.each do |image|
159
166
  validate_image!(image)
167
+ next if image[:format_id] == FORMAT_RUNTIME # payload slots are never mounted
168
+
160
169
  mount = image[:mount_point]
161
170
  Tebako.packaging_error(126, "duplicate mount point '#{mount}'") if seen[mount]
162
171
 
@@ -166,7 +175,7 @@ module Tebako
166
175
 
167
176
  def validate_image!(image)
168
177
  unless FORMAT_IDS.cover?(image[:format_id])
169
- Tebako.packaging_error(126, "invalid format_id #{image[:format_id]} (0..3 expected)")
178
+ Tebako.packaging_error(126, "invalid format_id #{image[:format_id]} (0..4 expected)")
170
179
  end
171
180
  return unless image[:mount_point].bytesize >= MOUNT_POINT_LEN
172
181
 
@@ -174,14 +183,21 @@ module Tebako
174
183
  "mount point '#{image[:mount_point][0, 32]}...' exceeds #{MOUNT_POINT_LEN - 1} bytes")
175
184
  end
176
185
 
186
+ def validate_runtime_sha256!(runtime_sha256)
187
+ return if runtime_sha256.nil? || runtime_sha256.match?(/\A[0-9a-f]{64}\z/)
188
+
189
+ Tebako.packaging_error(126, "runtime_sha256 must be 64 lowercase hex characters")
190
+ end
191
+
177
192
  def package_flags(lean)
178
193
  lean ? FLAG_LEAN : 0
179
194
  end
180
195
 
181
- def runtime_ref(lean, ruby_version, tebako_version)
196
+ def runtime_ref(lean, ruby_version, tebako_version, runtime_sha256)
182
197
  return "" unless lean
183
198
 
184
199
  ref = "ruby@#{ruby_version};tebako=#{tebako_version}"
200
+ ref += ";sha256=#{runtime_sha256}" if runtime_sha256
185
201
  if ref.bytesize >= RUNTIME_REF_LEN
186
202
  Tebako.packaging_error(126, "runtime_ref '#{ref}' exceeds #{RUNTIME_REF_LEN - 1} bytes")
187
203
  end
@@ -26,5 +26,5 @@
26
26
  # POSSIBILITY OF SUCH DAMAGE.
27
27
 
28
28
  module Tebako
29
- VERSION = "0.15.1"
29
+ VERSION = "0.15.3"
30
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tebako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.15.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -212,6 +212,7 @@ files:
212
212
  - include/tebako/tebako-prism.h
213
213
  - include/tebako/tpkg.h
214
214
  - lib/tebako.rb
215
+ - lib/tebako/bootstrap_manager.rb
215
216
  - lib/tebako/build_helpers.rb
216
217
  - lib/tebako/cache_manager.rb
217
218
  - lib/tebako/cli.rb