ocran 1.4.4 → 1.4.6

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.
@@ -26,24 +26,115 @@ module Ocran
26
26
 
27
27
  include BuildConstants, CommandOutput, HostConfigHelper
28
28
 
29
+ # Packed name of the interpreter when a cosmopolitan Ruby payload is
30
+ # used (--cosmo-ruby); the source APE is packed under this name.
31
+ COSMO_RUBY_EXE = "ruby.com"
32
+
33
+ # File name extensions of loadable native binaries that a gem may
34
+ # ship or build. None of them can be loaded by the cosmopolitan Ruby
35
+ # payload, which is statically linked and has no dlopen.
36
+ NATIVE_BINARY_EXTENSIONS = %w[.so .bundle .dll].freeze
37
+
38
+ # Native binaries a gem ships or has had built for it, i.e. the files
39
+ # that make it unusable under the cosmopolitan Ruby payload.
40
+ #
41
+ # spec.extensions alone does NOT identify a native gem: a precompiled
42
+ # platform gem (e.g. sqlite3-2.9.5-x86_64-linux-gnu) has an empty
43
+ # extensions array because nothing is compiled at install time, yet it
44
+ # ships a prebuilt sqlite3_native.so inside its lib directory. Both the
45
+ # gem directory and the extension directory (where RubyGems puts the
46
+ # products of a source build) are scanned.
47
+ def self.gem_native_binaries(spec)
48
+ ext_dir =
49
+ begin
50
+ spec.extension_dir
51
+ rescue StandardError
52
+ nil
53
+ end
54
+ pattern = "**/*{#{NATIVE_BINARY_EXTENSIONS.join(",")}}"
55
+ [spec.gem_dir, ext_dir].compact.uniq.flat_map { |dir|
56
+ next [] unless File.directory?(dir)
57
+
58
+ Dir.glob(pattern, base: dir).map { |rel| Pathname(File.join(dir, rel)) }
59
+ }.uniq
60
+ end
61
+
62
+ # The feature names a gem is required by, i.e. what has to answer
63
+ # inside the payload for the payload's own copy to serve in place of
64
+ # the host gem. RubyGems' convention maps a dash in a gem name to a
65
+ # directory separator in its primary feature (io-console provides
66
+ # "io/console"), and both spellings are seen in the wild, so both are
67
+ # offered as candidates.
68
+ def self.cosmo_gem_features(spec)
69
+ [spec.name, spec.name.tr("-", "/")].uniq
70
+ end
71
+
72
+ # Decides how a gem detected on the build host has to be treated when
73
+ # a cosmopolitan Ruby payload is packed (--cosmo-ruby). Returns a pair
74
+ # of a disposition and the gem's native binaries:
75
+ #
76
+ # [:pack, []] pure Ruby gem, pack it as usual
77
+ # [:payload_provides, files] native, but the payload provides the
78
+ # same library itself: skip the host copy
79
+ # and let the payload's own version serve
80
+ # [:incompatible, files] native and not provided by the payload:
81
+ # the build must fail
82
+ #
83
+ # A gem counts as provided by the payload when the payload has a
84
+ # gemspec of that name, OR when the payload can resolve the gem's
85
+ # primary feature (+provides_feature+, normally
86
+ # CosmoToolchain.resolvable_features against the payload). The second
87
+ # test matters because a gemspec is not what makes a library
88
+ # requirable: an extension statically linked into the APE, or a
89
+ # library in its embedded stdlib rather than in /zip/lib/ruby/gems,
90
+ # answers require with no gemspec at all. Keying only on gemspec names
91
+ # reports such a library as incompatible and refuses a build that would
92
+ # have worked - which is what happens to cgi and pathname, both
93
+ # compiled into the interpreter and both regular native gems on a
94
+ # recent host Ruby.
95
+ def self.cosmo_gem_disposition(spec, payload_gem_names, provides_feature = nil)
96
+ native_files = gem_native_binaries(spec)
97
+ return [:pack, native_files] if spec.extensions.empty? && native_files.empty?
98
+
99
+ if payload_gem_names.include?(spec.name) ||
100
+ (provides_feature && provides_feature.call(cosmo_gem_features(spec)))
101
+ [:payload_provides, native_files]
102
+ else
103
+ [:incompatible, native_files]
104
+ end
105
+ end
106
+
107
+ # Human readable reason why a gem counts as native, for build messages.
108
+ def self.cosmo_native_reason(spec, native_files)
109
+ reasons = []
110
+ reasons << "declares native extensions" if spec.extensions.any?
111
+ if native_files.any?
112
+ names = native_files.map { |file| File.basename(file) }.uniq
113
+ reasons << "ships prebuilt binaries (#{names.join(", ")})"
114
+ end
115
+ reasons.join(" and ")
116
+ end
117
+
29
118
  attr_reader :ruby_executable, :rubyopt
30
119
 
31
120
  def initialize(post_env, pre_env, option)
32
121
  @post_env, @pre_env, @option = post_env, pre_env, option
33
- @ruby_executable = @option.windowed? ? rubyw_exe : ruby_exe
122
+ @ruby_executable =
123
+ if @option.cosmo_ruby
124
+ COSMO_RUBY_EXE
125
+ else
126
+ @option.windowed? ? rubyw_exe : ruby_exe
127
+ end
34
128
 
35
129
  # Initializes @rubyopt with the user-intended RUBYOPT environment variable.
36
130
  # This ensures that RUBYOPT matches the user's initial settings before any
37
131
  # modifications that may occur during script execution.
132
+ #
133
+ # -I and -r entries that refer to build-machine paths (including the
134
+ # `-r<abs path>/bundler/setup` that Bundler adds under `bundle exec`
135
+ # since Ruby 3.2) are translated or removed at build time by
136
+ # RubyoptProcessor in #construct.
38
137
  @rubyopt = @option.rubyopt || pre_env.env["RUBYOPT"] || ""
39
-
40
- # Remove any absolute path to bundler/setup from RUBYOPT.
41
- # When building under `bundle exec`, RUBYOPT contains `-r/absolute/path/bundler/setup`.
42
- # That path doesn't exist inside the packed executable's environment, causing Ruby to
43
- # print "RubyGems were not loaded" / "did_you_mean was not loaded" warnings on startup.
44
- # We strip the flag regardless of install prefix because the gem may live in a user gem
45
- # directory that doesn't share a prefix with RbConfig::TOPDIR (e.g. on CI runners).
46
- @rubyopt = @rubyopt.gsub(/-r\S*\/bundler\/setup/, "").strip
47
138
  end
48
139
 
49
140
  # Resolves the common root directory prefix from an array of absolute paths.
@@ -72,7 +163,16 @@ module Ocran
72
163
 
73
164
  def detect_dlls
74
165
  if Gem.win_platform?
75
- require_relative "library_detector"
166
+ begin
167
+ require_relative "library_detector"
168
+ rescue LoadError => e
169
+ # LibraryDetector needs fiddle, a bundled gem since Ruby 3.5. In a
170
+ # Bundler context (e.g. building with --gemfile) requiring it is
171
+ # refused unless the Gemfile lists fiddle, so degrade to no DLL
172
+ # auto-detection instead of aborting the build.
173
+ warning "DLL auto-detection disabled (#{e.message}). Add fiddle to the Gemfile, or use --dll to include DLLs manually."
174
+ return []
175
+ end
76
176
  else
77
177
  require_relative "library_detector_posix"
78
178
  end
@@ -91,7 +191,7 @@ module Ocran
91
191
  end
92
192
  end
93
193
  if defined?(Gem)
94
- specs += Gem.loaded_specs.values
194
+ specs += application_loaded_specs
95
195
  # Now, we also detect gems that are not included in Gem.loaded_specs.
96
196
  # Therefore, we look for any loaded file from a gem path.
97
197
  specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
@@ -101,6 +201,123 @@ module Ocran
101
201
  specs
102
202
  end
103
203
 
204
+ # The gems RubyGems had activated when the dependency run ended, less
205
+ # those that are not the application's.
206
+ #
207
+ # Read from the snapshot taken with the loaded features, not from
208
+ # Gem.loaded_specs, which by now also holds what OCRAN activated for the
209
+ # build itself - fiddle for DLL detection, tempfile with tmpdir,
210
+ # fileutils, delegate and etc for the builder - and which every
211
+ # application used to get packed since 1.3.16 moved the build ahead of
212
+ # the gem scan. OCRAN's own gem and its runtime dependencies (fiddle) are
213
+ # dropped as well: the `ocran` command activates them before the
214
+ # dependency run starts, and since OCRA 1.3.2 they were packed into
215
+ # every application built from an installed OCRAN, although nothing in a
216
+ # packaged application loads OCRAN. Only activation is discounted: an
217
+ # application that does load one of them still gets it packed through
218
+ # detect_gems_from.
219
+ def application_loaded_specs
220
+ @application_loaded_specs ||= begin
221
+ excluded = foreign_bundle_gem_names | ocran_gem_names
222
+ @post_env.loaded_specs.reject { |spec| excluded.include?(spec.name) }
223
+ end
224
+ end
225
+
226
+ # The names of the gem this OCRAN runs from and of the gems it activated
227
+ # as its runtime dependencies. Empty when OCRAN does not run from an
228
+ # installed gem, as from a source checkout.
229
+ def ocran_gem_names
230
+ specs = @post_env.loaded_specs.to_h { |spec| [spec.name, spec] }
231
+ own = specs.each_value.find { |spec| Pathname(__dir__).subpath?(spec.gem_dir) }
232
+ names = Set.new
233
+ queue = own ? [own] : []
234
+ while (spec = queue.shift)
235
+ next unless names.add?(spec.name)
236
+
237
+ spec.runtime_dependencies.each do |dep|
238
+ queue << specs[dep.name] if specs.key?(dep.name)
239
+ end
240
+ end
241
+ names
242
+ end
243
+
244
+ # The gems RubyGems had already activated for a bundle that is not the
245
+ # application's, by the time OCRAN started.
246
+ #
247
+ # `bundle exec` activates every gem of its bundle before the command it
248
+ # runs executes a single line, so Gem.loaded_specs describes the build
249
+ # environment as much as the application. When the two bundles are the
250
+ # same - `bundle exec ocran app.rb` from the application's own directory,
251
+ # the ordinary case - that is exactly right and nothing is dropped. When
252
+ # they differ, packing the build environment's bundle adds tens of
253
+ # megabytes of code the application can never load: the packaged app runs
254
+ # under its own Gemfile, so gems from a foreign bundle are dead weight
255
+ # even when they are packed.
256
+ #
257
+ # Only activation is discounted, not use: anything the dependency run
258
+ # actually loaded is still found through $LOADED_FEATURES by
259
+ # detect_gems_from, and everything the application's Gemfile names is
260
+ # added by the Gemfile scan. This is the same rule that already applies
261
+ # to a build outside Bundler, where --gemfile is what pulls in gems the
262
+ # dependency run does not load.
263
+ def foreign_bundle_gem_names
264
+ @foreign_bundle_gem_names ||=
265
+ if foreign_build_bundle?
266
+ verbose "Ignoring #{@pre_env.activated_gems.size} gems activated by the build environment's bundle " \
267
+ "#{@pre_env.env["BUNDLE_GEMFILE"]}"
268
+ @pre_env.activated_gems.to_set
269
+ else
270
+ Set.new
271
+ end
272
+ end
273
+
274
+ # Whether OCRAN itself was started under a bundle other than the one the
275
+ # application runs under.
276
+ def foreign_build_bundle?
277
+ return false unless @pre_env.bundler_setup_loaded?
278
+
279
+ build_gemfile = @pre_env.env["BUNDLE_GEMFILE"]
280
+ return false if build_gemfile.nil? || build_gemfile.empty?
281
+
282
+ app_gemfile = @option.application_gemfile
283
+ return true if app_gemfile.nil?
284
+
285
+ !same_file?(build_gemfile, app_gemfile)
286
+ end
287
+
288
+ def same_file?(a, b)
289
+ File.identical?(a, b) || File.expand_path(a) == File.expand_path(b)
290
+ end
291
+
292
+ # Packed name of the file BUNDLER_SETUP is pointed at.
293
+ BUNDLER_SETUP_NOOP = Pathname("no_bundler_setup.rb")
294
+
295
+ # Keeps the environment of whoever launches the packaged application
296
+ # from dragging Bundler into it.
297
+ #
298
+ # A packaged application carries its own gems and its own Gemfile; the
299
+ # bundle of the machine it is started from means nothing to it, and
300
+ # anything of that bundle that survives into the process is fatal rather
301
+ # than merely wrong - Bundler aborts with GemNotFound as soon as it
302
+ # cannot materialize gems that were never packed. RUBYOPT is already
303
+ # overwritten wholesale, but that alone stopped being enough: RubyGems
304
+ # now requires the file named by BUNDLER_SETUP at interpreter startup,
305
+ # which is how current Bundler versions set a process up, and
306
+ # BUNDLE_GEMFILE would still send the application's own
307
+ # `require "bundler/setup"` at the wrong Gemfile.
308
+ #
309
+ # BUNDLER_SETUP names a file to require, so it cannot simply be blanked
310
+ # - an empty value is still truthy and RubyGems would raise trying to
311
+ # require it. It is pointed at an empty packed file instead. Bundler
312
+ # does treat empty BUNDLE_GEMFILE and BUNDLE_LOCKFILE as unset, which is
313
+ # what lets the application find the Gemfile packed beside it.
314
+ def neutralize_bundler_env(builder)
315
+ builder.touch(BUNDLER_SETUP_NOOP)
316
+ builder.set_env_path("BUNDLER_SETUP", BUNDLER_SETUP_NOOP)
317
+ builder.export("BUNDLE_GEMFILE", "")
318
+ builder.export("BUNDLE_LOCKFILE", "")
319
+ end
320
+
104
321
  def normalized_features
105
322
  features = @post_env.loaded_features.map { |feature| Pathname(feature) }
106
323
 
@@ -114,10 +331,12 @@ module Ocran
114
331
  # because rubygems.rb uses require_relative to load it.
115
332
  kernel_require_rel = "rubygems/core_ext/kernel_require.rb"
116
333
  unless features.any? { |f| f.to_posix.end_with?(kernel_require_rel) }
117
- # Prefer the location alongside the actually-loaded rubygems.rb, fall back to rubylibdir
118
- rubygems_feature = features.find { |f| f.to_posix.end_with?("/rubygems.rb") }
119
- candidate_dirs = []
120
- candidate_dirs << rubygems_feature.dirname if rubygems_feature
334
+ # Prefer the location alongside the actually-loaded rubygems.rb, fall back to
335
+ # rubylibdir. Consider every feature ending in "/rubygems.rb", because a plain
336
+ # suffix match can also hit unrelated files such as bundler's
337
+ # lib/bundler/source/rubygems.rb (loaded before rubygems.rb under bundle exec);
338
+ # the existence check below skips candidates without the core_ext file.
339
+ candidate_dirs = features.select { |f| f.to_posix.end_with?("/rubygems.rb") }.map(&:dirname)
121
340
  candidate_dirs << Pathname(RbConfig::CONFIG["rubylibdir"])
122
341
  candidate_dirs.each do |base_dir|
123
342
  kernel_require_path = base_dir / kernel_require_rel
@@ -147,17 +366,54 @@ module Ocran
147
366
  end
148
367
  end
149
368
 
369
+ # True when the packed cosmopolitan Ruby can resolve any of the given
370
+ # features itself, i.e. when a host gem providing them does not have
371
+ # to be packed. Answers are memoized per feature: probing costs one
372
+ # run of the payload interpreter, and only native gems the payload has
373
+ # no gemspec for ever get here.
374
+ def cosmo_payload_provides_feature?(features)
375
+ @cosmo_feature_cache ||= {}
376
+ unknown = features.reject { |feature| @cosmo_feature_cache.key?(feature) }
377
+ unless unknown.empty?
378
+ resolved = CosmoToolchain.resolvable_features(@option.cosmo_ruby, unknown)
379
+ unknown.each { |feature| @cosmo_feature_cache[feature] = resolved.include?(feature) }
380
+ end
381
+ features.any? { |feature| @cosmo_feature_cache[feature] }
382
+ end
383
+
150
384
  def construct(builder)
151
385
  # Store the currently loaded files
152
386
  features = normalized_features
153
387
 
388
+ # With --cosmo-ruby, run the payload interpreter once on the build
389
+ # host: this validates that it works, provides its embedded gem
390
+ # directory (needed for GEM_PATH below) and its version for the
391
+ # host-vs-payload skew warning. Dependency detection has already
392
+ # run under the *host* Ruby, so stdlib/gem resolution may differ
393
+ # when the versions diverge.
394
+ if @option.cosmo_ruby
395
+ # Kernel#load, matching Option#load_cosmo_toolchain: the file may
396
+ # already have been loaded that way at option-parse time, and
397
+ # require_relative would then run it a second time.
398
+ load File.expand_path("cosmo_toolchain.rb", __dir__) unless defined? CosmoToolchain
399
+ @cosmo_ruby_info = CosmoToolchain.query_ruby(@option.cosmo_ruby)
400
+ say "Packaging cosmopolitan Ruby #{@cosmo_ruby_info[:version]} (#{@option.cosmo_ruby})"
401
+ if RUBY_VERSION.split(".").take(2) != @cosmo_ruby_info[:version].split(".").take(2)
402
+ warning "Dependency detection ran under the host Ruby #{RUBY_VERSION}, but the packed cosmopolitan Ruby is #{@cosmo_ruby_info[:version]}; stdlib and gem behavior may differ between these versions"
403
+ end
404
+ end
405
+
154
406
  # If net/http was loaded but openssl wasn't (it is only required lazily
155
407
  # at the point of an actual HTTPS connection), require it now inside the
156
408
  # OCRAN build process so that every transitive dependency — openssl.rb,
157
409
  # digest.so, and any other files pulled in by the extension — appears in
158
410
  # $LOADED_FEATURES and gets bundled alongside the application.
411
+ # Skipped with --cosmo-ruby: the payload interpreter carries its own
412
+ # (statically linked) openssl, and the host's files would be excluded
413
+ # from the package anyway.
159
414
  openssl_so = Pathname(RbConfig::CONFIG["archdir"]) / "openssl.so"
160
- if openssl_so.exist? &&
415
+ if !@option.cosmo_ruby &&
416
+ openssl_so.exist? &&
161
417
  features.any? { |f| f.to_posix.end_with?("/net/http.rb") } &&
162
418
  features.none? { |f| f == openssl_so }
163
419
  say "Auto-loading openssl (net/http loaded but openssl not yet required)"
@@ -175,53 +431,94 @@ module Ocran
175
431
 
176
432
  # Add the ruby executable and DLL
177
433
  say "Adding ruby executable #{ruby_executable}"
178
- ruby_source = bindir / ruby_executable
179
- if !Gem.win_platform? && File.binread(ruby_source, 2) == "#!"
180
- # On some distros (e.g. Fedora), bindir/ruby is a dispatcher shell
181
- # script ("rubypick") rather than the interpreter itself, which
182
- # cannot run on a system without Ruby. Pack the currently running
183
- # interpreter binary under the expected name instead.
184
- real_ruby = Pathname("/proc/self/exe")
185
- raise "#{ruby_source} is a wrapper script and the real interpreter could not be determined" unless real_ruby.exist?
186
-
187
- say "#{ruby_source} is a wrapper script; packing #{real_ruby.realpath} instead"
188
- builder.copy_to_bin(real_ruby.realpath, ruby_executable)
434
+ if @option.cosmo_ruby
435
+ # The cosmopolitan Ruby APE is fully self-contained: a static
436
+ # binary with the standard library embedded in its ZIP store
437
+ # (/zip/lib/ruby/...). No libruby, no shared libraries and no
438
+ # LD_LIBRARY_PATH are needed — pack the single file and be done.
439
+ #
440
+ # Except in ZIP packaging mode, where the output IS that binary and
441
+ # the application is injected into it: packing a copy of the
442
+ # interpreter into itself would double the size of the executable
443
+ # for nothing.
444
+ if @option.cosmo_zip?
445
+ say "Injecting the application into the ZIP store of #{@option.cosmo_ruby}"
446
+ else
447
+ builder.copy_to_bin(Pathname(@option.cosmo_ruby), ruby_executable)
448
+ end
189
449
  else
190
- builder.copy_to_bin(ruby_source, ruby_executable)
191
- end
192
- if libruby_so
193
- # On POSIX systems, libruby.so is in libdir; on Windows, it's in bindir
194
- libruby_src = Gem.win_platform? ? bindir / libruby_so : libdir / libruby_so
195
- builder.copy_to_bin(libruby_src, libruby_so)
196
-
197
- # On POSIX systems, create symlinks (aliases) for libruby.so
198
- unless Gem.win_platform?
199
- libruby_aliases.each do |libruby_alias|
200
- builder.symlink_in_bin(libruby_so, libruby_alias)
450
+ ruby_source = bindir / ruby_executable
451
+ if !Gem.win_platform? && File.binread(ruby_source, 2) == "#!"
452
+ # On some distros (e.g. Fedora), bindir/ruby is a dispatcher shell
453
+ # script ("rubypick") rather than the interpreter itself, which
454
+ # cannot run on a system without Ruby. Pack the currently running
455
+ # interpreter binary under the expected name instead.
456
+ real_ruby = Pathname("/proc/self/exe")
457
+ raise "#{ruby_source} is a wrapper script and the real interpreter could not be determined" unless real_ruby.exist?
458
+
459
+ say "#{ruby_source} is a wrapper script; packing #{real_ruby.realpath} instead"
460
+ builder.copy_to_bin(real_ruby.realpath, ruby_executable)
461
+ else
462
+ builder.copy_to_bin(ruby_source, ruby_executable)
463
+ end
464
+ if libruby_so
465
+ # On POSIX systems, libruby.so is in libdir; on Windows, it's in bindir
466
+ libruby_src = Gem.win_platform? ? bindir / libruby_so : libdir / libruby_so
467
+ builder.copy_to_bin(libruby_src, libruby_so)
468
+
469
+ # On POSIX systems, create symlinks (aliases) for libruby.so
470
+ unless Gem.win_platform?
471
+ libruby_aliases.each do |libruby_alias|
472
+ builder.symlink_in_bin(libruby_so, libruby_alias)
473
+ end
201
474
  end
202
475
  end
203
- end
204
476
 
205
- # On POSIX systems, set LD_LIBRARY_PATH to find bundled shared libraries
206
- unless Gem.win_platform?
207
- extract_bin = File.join(EXTRACT_ROOT, BINDIR.to_s)
208
- builder.export("LD_LIBRARY_PATH", extract_bin)
209
- if RUBY_PLATFORM.include?("darwin")
210
- builder.export("DYLD_LIBRARY_PATH", extract_bin)
477
+ # On POSIX systems, set LD_LIBRARY_PATH to find bundled shared libraries
478
+ unless Gem.win_platform?
479
+ extract_bin = File.join(EXTRACT_ROOT, BINDIR.to_s)
480
+ builder.export("LD_LIBRARY_PATH", extract_bin)
481
+ if RUBY_PLATFORM.include?("darwin")
482
+ builder.export("DYLD_LIBRARY_PATH", extract_bin)
483
+ end
211
484
  end
212
485
  end
213
486
 
214
487
  # Windows-only: Add detected DLLs
215
488
  if Gem.win_platform? && @option.auto_detect_dlls?
489
+ # The Windows loader resolves the imports of a native extension from
490
+ # the extension's own directory, the application directory of the
491
+ # packed ruby.exe (bin) plus its SxS assembly (ruby_builtin_dlls),
492
+ # and the system directories. PATH is not consulted on hardened
493
+ # systems, and the AddDllDirectory mechanism gems use through
494
+ # ruby_installer/runtime is not available in a packed app. A detected
495
+ # DLL that lives anywhere else - a gem's bundled library (e.g.
496
+ # FreeTDS under tiny_tds' ports/), a devkit's msys64 tree inside the
497
+ # Ruby prefix, or a directory outside the prefix entirely - is packed
498
+ # only at a location the loader never searches, and the application
499
+ # dies with LoadError on machines where nothing masks the gap. So
500
+ # additionally bundle a copy of every such DLL into bin, next to
501
+ # ruby.exe, mirroring what the Linux branch below achieves with
502
+ # LD_LIBRARY_PATH. DLLs from the Windows directory always come from
503
+ # the target system and are never bundled.
504
+ windows_dir = Pathname(ENV["SystemRoot"] || "C:/Windows")
505
+ dlls_in_bin = Set.new
506
+ add_dll_to_bin = proc do |dll|
507
+ next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
508
+
509
+ say "Adding detected DLL #{dll} to bin"
510
+ builder.copy_to_bin(dll, dll.basename)
511
+ end
512
+
216
513
  detect_dlls.each do |dll|
217
- next unless dll.subpath?(exec_prefix) && dll.extname?(".dll") && dll.basename != libruby_so
514
+ next unless dll.extname?(".dll") && dll.basename != libruby_so
515
+ next if dll.subpath?(windows_dir)
218
516
 
219
- say "Adding detected DLL #{dll}"
220
517
  if dll.subpath?(exec_prefix)
518
+ say "Adding detected DLL #{dll}"
221
519
  builder.duplicate_to_exec_prefix(dll)
222
- else
223
- builder.copy_to_bin(dll, dll.basename)
224
520
  end
521
+ add_dll_to_bin.call(dll)
225
522
  end
226
523
 
227
524
  # Proactively include companion DLLs for loaded native extensions.
@@ -229,7 +526,10 @@ module Ocran
229
526
  # directory (e.g., libssl-3-x64.dll alongside openssl.so) that are
230
527
  # loaded lazily on first use. Scanning .so directories ensures those
231
528
  # DLLs are bundled even when the extension is required but not
232
- # exercised during the OCRAN dependency scan.
529
+ # exercised during the OCRAN dependency scan. They also go into bin:
530
+ # a copy in archdir only helps extensions in archdir itself, while
531
+ # the same extension packed at a gem path (openssl and psych are
532
+ # gems since Ruby 3.x) resolves its imports from bin.
233
533
  features.select { |f| f.extname?(".so") && f.subpath?(exec_prefix) }
234
534
  .map(&:dirname).uniq
235
535
  .each do |dir|
@@ -237,6 +537,7 @@ module Ocran
237
537
  next unless path.file? && path.extname?(".dll")
238
538
  say "Adding companion DLL #{path}"
239
539
  builder.duplicate_to_exec_prefix(path)
540
+ add_dll_to_bin.call(path)
240
541
  end
241
542
  end
242
543
  end
@@ -247,8 +548,8 @@ module Ocran
247
548
  # points LD_LIBRARY_PATH at the packed bin directory). Core glibc
248
549
  # libraries and the loader are never bundled - they must come from the
249
550
  # target system. Ruby native extensions are packed as features, not
250
- # here.
251
- if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls?
551
+ # here. Not needed with --cosmo-ruby: the APE payload is static.
552
+ if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls? && !@option.cosmo_ruby
252
553
  feature_set = features.to_set
253
554
  feature_realpaths = features.filter_map { |f| f.realpath rescue nil }.to_set
254
555
  # Ruby native extensions live in these directories and are packed as
@@ -298,7 +599,7 @@ module Ocran
298
599
  archdir = Pathname(RbConfig::CONFIG["archdir"])
299
600
  sxs_manifest_dirs << archdir if archdir.exist? && archdir.subpath?(exec_prefix)
300
601
  if defined?(Gem)
301
- Gem.loaded_specs.each_value do |spec|
602
+ application_loaded_specs.each do |spec|
302
603
  next if spec.extensions.empty?
303
604
  ext_dir = Pathname(spec.extension_dir)
304
605
  sxs_manifest_dirs << ext_dir if ext_dir.exist? && ext_dir.subpath?(exec_prefix)
@@ -319,6 +620,11 @@ module Ocran
319
620
  end
320
621
  end
321
622
 
623
+ # Gem directories whose packing was skipped because the cosmopolitan
624
+ # Ruby payload provides the gem itself; loaded features from these
625
+ # directories must not be packed either.
626
+ cosmo_skipped_gem_dirs = []
627
+
322
628
  # Searches for features that are loaded from gems, then produces a
323
629
  # list of files included in those gems' manifests. Also returns a
324
630
  # list of original features that caused those gems to be included.
@@ -333,13 +639,80 @@ module Ocran
333
639
  next []
334
640
  end
335
641
 
642
+ if @option.cosmo_ruby
643
+ # Default gems of the *host* Ruby are part of its stdlib; the
644
+ # cosmopolitan Ruby ships its own stdlib and default/bundled
645
+ # gems in its embedded ZIP store, so do not pack them (a host
646
+ # 3.x copy would shadow the payload's version).
647
+ if spec.respond_to?(:default_gem?) && spec.default_gem?
648
+ verbose "Skipping default gem #{spec.full_name} (provided by the cosmopolitan Ruby's embedded stdlib)"
649
+ next []
650
+ end
651
+ # Native gems compile (or were precompiled) against a host Ruby
652
+ # ABI and platform; they cannot load under the x86_64-cosmo
653
+ # payload, which is statically linked and cannot dlopen. This
654
+ # covers both source-installed gems (spec.extensions) and
655
+ # precompiled platform gems, which declare no extensions but
656
+ # ship their .so inside the gem directory.
657
+ # When the payload provides the same gem itself (e.g. json,
658
+ # psych are statically linked into the APE), skip the host copy
659
+ # so the payload's own version is used — packing the host .rb
660
+ # files would shadow the payload's and could mismatch the
661
+ # linked-in C extension. Otherwise fail clearly rather than
662
+ # produce a broken executable.
663
+ disposition, native_files = self.class.cosmo_gem_disposition(
664
+ spec, @cosmo_ruby_info[:gem_names], method(:cosmo_payload_provides_feature?)
665
+ )
666
+ if disposition != :pack
667
+ reason = self.class.cosmo_native_reason(spec, native_files)
668
+ if disposition == :payload_provides
669
+ provided =
670
+ if @cosmo_ruby_info[:gem_names].include?(spec.name)
671
+ "its own #{spec.name}"
672
+ else
673
+ "#{spec.name} without a gemspec (linked in, or part of its embedded stdlib)"
674
+ end
675
+ say "Skipping native gem #{spec.full_name} (#{reason}): the cosmopolitan Ruby provides #{provided}"
676
+ cosmo_skipped_gem_dirs << Pathname(spec.gem_dir) if File.directory?(spec.gem_dir)
677
+ ext_dir =
678
+ begin
679
+ spec.extension_dir
680
+ rescue StandardError
681
+ nil
682
+ end
683
+ cosmo_skipped_gem_dirs << Pathname(ext_dir) if ext_dir && File.directory?(ext_dir)
684
+ next []
685
+ end
686
+ raise "Gem #{spec.full_name} is native (#{reason}) and cannot run under the packed cosmopolitan Ruby (x86_64-cosmo, static): exclude the gem or package without --cosmo-ruby"
687
+ end
688
+ end
689
+
336
690
  # Add gemspec files
691
+ local_gem_dir = nil
337
692
  if spec_file.subpath?(exec_prefix)
338
693
  builder.duplicate_to_exec_prefix(spec_file)
339
694
  elsif (gem_path = GemSpecQueryable.find_gem_path(spec_file))
340
695
  builder.duplicate_to_gem_home(spec_file, gem_path)
341
696
  else
342
- raise "Gem spec #{spec_file} does not exist in the Ruby installation. Don't know where to put it."
697
+ # Local development gems (Bundler `gemspec` or `path:` directives)
698
+ # keep their gemspec inside the project tree, outside both the Ruby
699
+ # installation and every gem path, so there is no installed gem
700
+ # layout to mirror. Pack them into GEMDIR as if they were installed
701
+ # there: generate the spec from the in-memory specification (the
702
+ # on-disk gemspec often uses dynamic constructs such as
703
+ # `git ls-files` that would fail in the packed app) and pack the
704
+ # gem's files under gems/<full_name>/ below.
705
+ say "Including local development gem #{spec.full_name} from #{spec_file.dirname}"
706
+ local_gem_dir = Pathname(spec.gem_dir)
707
+ builder.copy_to_gem(generate_gemspec_file(spec), Pathname("specifications") / "#{spec.full_name}.gemspec")
708
+ # RubyGems refuses to activate a gem with extensions unless its
709
+ # gem.build_complete marker exists. The extension files themselves
710
+ # are packed via the loaded features or the extension-dir mirroring
711
+ # below.
712
+ if spec.extensions.any?
713
+ api_version = Gem.respond_to?(:extension_api_version) ? Gem.extension_api_version : Gem.ruby_api_version
714
+ builder.touch(GEMDIR / "extensions" / Gem::Platform.local.to_s / api_version / spec.full_name / "gem.build_complete")
715
+ end
343
716
  end
344
717
 
345
718
  spec_dir = spec_file.dirname
@@ -399,6 +772,20 @@ module Ocran
399
772
  verbose "\t:files (resource_files) count: #{resource_count}"
400
773
 
401
774
  actual_files = spec.find_gem_files(include, features)
775
+
776
+ # Safety net: gems reaching this point are pure Ruby as far as
777
+ # their gem and extension directories go (see the disposition
778
+ # check above), but a file list can still pull in a native binary
779
+ # from elsewhere. It cannot load under the cosmopolitan payload,
780
+ # so exclude it loudly.
781
+ if @option.cosmo_ruby
782
+ native_files = actual_files.select { |f| NATIVE_BINARY_EXTENSIONS.any? { |ext| f.extname?(ext) } }
783
+ if native_files.any?
784
+ warning "Gem #{spec.full_name} contains native binaries that cannot run under the packed cosmopolitan Ruby; excluding: #{native_files.map(&:basename).join(", ")}"
785
+ actual_files -= native_files
786
+ end
787
+ end
788
+
402
789
  say "\t#{actual_files.size} files, #{actual_files.sum(0, &:size)} bytes"
403
790
 
404
791
  # Decide where to put gem files, either the system gem folder, or
@@ -408,6 +795,10 @@ module Ocran
408
795
  builder.duplicate_to_exec_prefix(gemfile)
409
796
  elsif (gem_path = GemSpecQueryable.find_gem_path(gemfile))
410
797
  builder.duplicate_to_gem_home(gemfile, gem_path)
798
+ elsif local_gem_dir && gemfile.subpath?(local_gem_dir)
799
+ # Mirror local development gem files into the packed GEM_HOME
800
+ # under the gem directory matching the generated specification.
801
+ builder.copy_to_gem(gemfile, Pathname("gems") / spec.full_name / gemfile.relative_path_from(local_gem_dir))
411
802
  else
412
803
  raise "Don't know where to put gemfile #{gemfile}"
413
804
  end
@@ -481,7 +872,9 @@ module Ocran
481
872
  end
482
873
 
483
874
  # If requested, add all ruby standard libraries
484
- if @option.add_all_core?
875
+ if @option.add_all_core? && @option.cosmo_ruby
876
+ say "Skipping host core libraries (--add-all-core): the cosmopolitan Ruby embeds its own standard library"
877
+ elsif @option.add_all_core?
485
878
  say "Will include all ruby core libraries"
486
879
  all_core_dir.each do |path|
487
880
  # Match the load path against standard library, site_ruby, and vendor_ruby paths
@@ -497,7 +890,10 @@ module Ocran
497
890
  end
498
891
 
499
892
  # Include encoding support files
500
- if @option.add_all_encoding?
893
+ if @option.cosmo_ruby
894
+ # Encoding extensions are statically linked into the payload.
895
+ say "Encoding support is embedded in the cosmopolitan Ruby"
896
+ elsif @option.add_all_encoding?
501
897
  @post_env.load_path.each do |load_path|
502
898
  load_path = Pathname(@post_env.expand_path(load_path))
503
899
  next unless load_path.subpath?(exec_prefix)
@@ -535,6 +931,23 @@ module Ocran
535
931
  pre_working_directory = Pathname(@pre_env.pwd)
536
932
  working_directory = Pathname(@post_env.pwd)
537
933
  features.each do |feature|
934
+ # With --cosmo-ruby, files of the host Ruby installation must not
935
+ # be packed: the payload interpreter resolves the standard library
936
+ # from its embedded ZIP store, and a packed host-version copy (or
937
+ # a host-ABI native extension) would be wrong for it.
938
+ if @option.cosmo_ruby
939
+ if feature.subpath?(exec_prefix)
940
+ verbose "\tlibfile: #{feature} -> skipped (host Ruby installation; the cosmopolitan Ruby uses its embedded stdlib)"
941
+ next
942
+ elsif cosmo_skipped_gem_dirs.any? { |dir| feature.subpath?(dir) }
943
+ verbose "\tlibfile: #{feature} -> skipped (gem provided by the cosmopolitan Ruby)"
944
+ next
945
+ elsif feature.extname?(".so") || feature.extname?(".bundle")
946
+ warning "Excluding native extension file #{feature}: native extensions cannot run under the packed cosmopolitan Ruby"
947
+ next
948
+ end
949
+ end
950
+
538
951
  load_path = @post_env.find_load_path(feature)
539
952
  if load_path.nil?
540
953
  verbose "\tlibfile: #{feature} -> src (no load path)"
@@ -649,8 +1062,24 @@ module Ocran
649
1062
  end
650
1063
  end
651
1064
 
1065
+ # Translate -I and -r entries in RUBYOPT that refer to build-machine
1066
+ # paths into the packed layout (GitHub issue #20). Ruby cannot parse
1067
+ # quoted arguments inside RUBYOPT and the runtime extraction directory
1068
+ # may contain spaces, so instead of rewriting the paths inside RUBYOPT
1069
+ # the translated entries are applied by a generated launcher script
1070
+ # (see generate_rubyopt_launcher) where they become plain Ruby string
1071
+ # literals.
1072
+ require_relative "rubyopt_processor"
1073
+ rubyopt_result = RubyoptProcessor.new(rubyopt).translate do |path|
1074
+ packed_rubyopt_path(Pathname(path).cleanpath, inst_src_prefix)
1075
+ end
1076
+ rubyopt_result.dropped.each do |entry|
1077
+ say "Removing #{entry} from RUBYOPT (path is not part of the package)"
1078
+ end
1079
+
652
1080
  # Set environment variable
653
- builder.export("RUBYOPT", rubyopt)
1081
+ builder.export("RUBYOPT", rubyopt_result.rubyopt)
1082
+ neutralize_bundler_env(builder)
654
1083
  # Add the load path that are required with the correct path after
655
1084
  # src_prefix was adjusted.
656
1085
  load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
@@ -660,7 +1089,9 @@ module Ocran
660
1089
  # host, which doesn't exist on other systems (e.g., Docker with no Ruby).
661
1090
  # By adding the extract-dir equivalents of rubylibdir, sitelibdir, etc. to
662
1091
  # RUBYLIB, Ruby can find rubygems and the standard library in the packed tree.
663
- unless Gem.win_platform?
1092
+ # Not with --cosmo-ruby: the host stdlib is not packed at all, and the
1093
+ # payload finds its own stdlib in its embedded ZIP store.
1094
+ unless Gem.win_platform? || @option.cosmo_ruby
664
1095
  # Use the build Ruby's actual default load path in addition to the
665
1096
  # RbConfig directories: some distros compile in extra entries that
666
1097
  # RbConfig does not expose (e.g. Fedora's /usr/share/rubygems, where
@@ -697,14 +1128,99 @@ module Ocran
697
1128
  # when the directory does not exist in the packed layout - so always
698
1129
  # create the packed prefix gem dirs, even when no specs landed there.
699
1130
  prefix_gem_dirs.each { |dir| builder.mkdir(dir) }
1131
+ if @option.cosmo_ruby
1132
+ # When GEM_PATH is set, RubyGems no longer scans its compiled-in
1133
+ # default directory — which for the cosmopolitan Ruby is the /zip
1134
+ # store inside the binary, where its bundled gems live. Keep it
1135
+ # reachable by appending it explicitly.
1136
+ gem_paths << @cosmo_ruby_info[:default_gem_dir]
1137
+ end
700
1138
  builder.set_env_path("GEM_PATH", *gem_paths)
701
1139
 
702
1140
  # Add the opcode to launch the script
703
1141
  installed_ruby_exe = BINDIR / ruby_executable
704
1142
  target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
1143
+ if rubyopt_result.translated?
1144
+ target_script = generate_rubyopt_launcher(builder, target_script, rubyopt_result)
1145
+ end
705
1146
  builder.exec(installed_ruby_exe, target_script, *@option.argv)
706
1147
  end
707
1148
 
1149
+ # Maps an absolute build-machine path to its location (relative to the
1150
+ # extraction root) inside the packed application, mirroring where
1151
+ # #construct places files. Returns nil when the path is not packed.
1152
+ def packed_rubyopt_path(path, src_prefix)
1153
+ require_relative "gem_spec_queryable"
1154
+
1155
+ if path.subpath?(exec_prefix)
1156
+ path.relative_path_from(exec_prefix)
1157
+ elsif (gem_path = GemSpecQueryable.find_gem_path(path))
1158
+ GEMDIR / path.relative_path_from(gem_path)
1159
+ elsif path.subpath?(src_prefix)
1160
+ SRCDIR / path.relative_path_from(src_prefix)
1161
+ end
1162
+ end
1163
+ private :packed_rubyopt_path
1164
+
1165
+ RUBYOPT_LAUNCHER_NAME = "ocran-rubyopt-launcher.rb"
1166
+
1167
+ # Writes a launcher script next to the packed application script that
1168
+ # applies the translated RUBYOPT -I/-r entries (as plain Ruby string
1169
+ # literals resolved against the extraction directory at runtime) and
1170
+ # then loads the original script. Returns the packed path of the
1171
+ # launcher, which becomes the script the stub executes.
1172
+ def generate_rubyopt_launcher(builder, target_script, result)
1173
+ say "Generating launcher script for RUBYOPT entries with translated paths"
1174
+ launcher_target = target_script.dirname / RUBYOPT_LAUNCHER_NAME
1175
+
1176
+ # Relative path from the launcher's directory up to the extraction root.
1177
+ depth = target_script.dirname.each_filename.count { |name| name != "." }
1178
+ root_rel = depth.zero? ? "." : ([".."] * depth).join("/")
1179
+
1180
+ lines = []
1181
+ lines << "# frozen_string_literal: true"
1182
+ lines << "# Generated by OCRAN. Applies -I/-r options that were given in RUBYOPT"
1183
+ lines << "# with build-machine paths, translated to the extraction directory."
1184
+ lines << "# They cannot remain in RUBYOPT because Ruby does not support quoting"
1185
+ lines << "# there and the extraction path may contain spaces."
1186
+ lines << "ocran_root = File.expand_path(#{root_rel.dump}, __dir__)"
1187
+ lines << "$0 = File.expand_path(#{target_script.basename.to_s.dump}, __dir__)"
1188
+ result.load_paths.reverse_each do |dir|
1189
+ lines << "$LOAD_PATH.unshift(File.expand_path(#{dir.to_posix.dump}, ocran_root))"
1190
+ end
1191
+ result.requires.each do |feature|
1192
+ lines << "require File.expand_path(#{feature.to_posix.dump}, ocran_root)"
1193
+ end
1194
+ lines << "load $0"
1195
+
1196
+ require "tempfile"
1197
+ launcher_file = Tempfile.new(["ocran-rubyopt-launcher", ".rb"])
1198
+ launcher_file.write(lines.join("\n") + "\n")
1199
+ launcher_file.close
1200
+ verbose File.read(launcher_file.path)
1201
+ # Keep a reference so the temporary file survives until the build
1202
+ # finishes; the Inno Setup builder reads source files only when the
1203
+ # installer is compiled, after #construct has returned.
1204
+ @rubyopt_launcher_file = launcher_file
1205
+
1206
+ builder.cp(launcher_file.path, launcher_target)
1207
+ launcher_target
1208
+ end
1209
+ private :generate_rubyopt_launcher
1210
+ # Writes the in-memory gem specification to a temporary file and returns
1211
+ # the file's path, for packing gemspecs that cannot be copied verbatim
1212
+ # from disk (e.g. local development gems). The Tempfile object is
1213
+ # retained because some builders (e.g. InnoSetupScriptBuilder) read
1214
+ # their source files only after construction has completed.
1215
+ def generate_gemspec_file(spec)
1216
+ require "tempfile"
1217
+ file = Tempfile.new(["#{spec.full_name}-", ".gemspec"])
1218
+ file.write(spec.to_ruby)
1219
+ file.close
1220
+ (@generated_gemspec_files ||= []) << file
1221
+ file.path
1222
+ end
1223
+
708
1224
  def to_proc
709
1225
  method(:construct).to_proc
710
1226
  end
@@ -762,6 +1278,23 @@ module Ocran
762
1278
  say "Finished building installer file"
763
1279
  end
764
1280
 
1281
+ # Returns the path to the stub built from source with cosmocc when
1282
+ # --cosmo was given, or nil to use the pre-built stub shipped with
1283
+ # the gem. The build result is memoized (and CosmoToolchain caches
1284
+ # compiled stubs across runs), so multiple stubs per build (e.g.
1285
+ # wrapper executables) compile at most once.
1286
+ def cosmo_stub_path
1287
+ return nil unless @option.cosmo_cc
1288
+
1289
+ @cosmo_stub_path ||= begin
1290
+ load File.expand_path("cosmo_toolchain.rb", __dir__) unless defined? CosmoToolchain
1291
+ say "Building launcher stub from source with cosmocc (#{@option.cosmo_cc})"
1292
+ path = CosmoToolchain.build_stub(@option.cosmo_cc)
1293
+ say "Using APE stub #{path}"
1294
+ path
1295
+ end
1296
+ end
1297
+
765
1298
  # Builds the small RUN_IN_EXE_DIR wrapper stub that starts the deployed
766
1299
  # application directly from the directory the wrapper resides in.
767
1300
  def build_wrapper_exe(wrapper_path)
@@ -769,10 +1302,12 @@ module Ocran
769
1302
  say "Build wrapper executable #{wrapper_path.basename}"
770
1303
  StubBuilder.new(wrapper_path,
771
1304
  chdir_before: @option.chdir_before?,
1305
+ chdir_to_exe_dir: @option.chdir_exe_dir?,
772
1306
  debug_mode: @option.enable_debug_mode?,
773
1307
  gui_mode: @option.windowed?,
774
1308
  icon_path: @option.icon_filename,
775
- run_in_exe_dir: true) do |stub|
1309
+ run_in_exe_dir: true,
1310
+ stub_path: cosmo_stub_path) do |stub|
776
1311
  yield(stub)
777
1312
  end
778
1313
  end
@@ -830,11 +1365,13 @@ module Ocran
830
1365
 
831
1366
  StubBuilder.new(executable_path,
832
1367
  chdir_before: @option.chdir_before?,
1368
+ chdir_to_exe_dir: @option.chdir_exe_dir?,
833
1369
  debug_extract: @option.enable_debug_extract?,
834
1370
  debug_mode: @option.enable_debug_mode?,
835
1371
  enable_compression: @option.enable_compression?,
836
1372
  gui_mode: false,
837
1373
  icon_path: nil,
1374
+ stub_path: cosmo_stub_path,
838
1375
  &to_proc) => builder
839
1376
 
840
1377
  if @option.icon_filename
@@ -870,6 +1407,40 @@ module Ocran
870
1407
  say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
871
1408
  end
872
1409
 
1410
+ # Builds the executable by copying the cosmopolitan Ruby and injecting
1411
+ # the application into its ZIP store (--cosmo-ruby with an interpreter
1412
+ # that runs an embedded /zip/main.rb). No compiler runs, no launcher
1413
+ # stub is involved, and the resulting binary unpacks nothing when it
1414
+ # starts.
1415
+ def build_cosmo_zip_exe
1416
+ require_relative "zip_payload_builder"
1417
+
1418
+ output = @option.output_executable
1419
+ ZipPayloadBuilder.new(output,
1420
+ cosmo_ruby: @option.cosmo_ruby,
1421
+ chdir_before: @option.chdir_before?,
1422
+ debug_mode: @option.enable_debug_mode?,
1423
+ &to_proc) => builder
1424
+
1425
+ builder.ignored_symlinks.each do |link_path, target|
1426
+ verbose "Skipping symlink #{link_path} -> #{target} (ZIP members cannot be symlinks)"
1427
+ end
1428
+
1429
+ if @option.icon_filename
1430
+ warning "--icon has no effect in this mode: the executable is a copy of the cosmopolitan Ruby, whose resources OCRAN does not rewrite"
1431
+ end
1432
+ if @option.enable_debug_extract?
1433
+ warning "--debug-extract has no effect in this mode: nothing is extracted, the application is read from the executable's own ZIP store"
1434
+ end
1435
+
1436
+ _, _, unsupported = ZipPayloadBuilder.parse_rubyopt(rubyopt)
1437
+ unless unsupported.empty?
1438
+ warning "RUBYOPT #{unsupported.join(" ")} cannot be applied when the application is packed into the interpreter's ZIP store (the interpreter is already running); only -I and -r are replayed"
1439
+ end
1440
+
1441
+ say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
1442
+ end
1443
+
873
1444
  def build_stab_exe
874
1445
  require_relative "stub_builder"
875
1446
 
@@ -879,11 +1450,13 @@ module Ocran
879
1450
 
880
1451
  StubBuilder.new(@option.output_executable,
881
1452
  chdir_before: @option.chdir_before?,
1453
+ chdir_to_exe_dir: @option.chdir_exe_dir?,
882
1454
  debug_extract: @option.enable_debug_extract?,
883
1455
  debug_mode: @option.enable_debug_mode?,
884
1456
  enable_compression: @option.enable_compression?,
885
1457
  gui_mode: @option.windowed?,
886
1458
  icon_path: @option.icon_filename,
1459
+ stub_path: cosmo_stub_path,
887
1460
  &to_proc) => builder
888
1461
  say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
889
1462
  say "After decompression, the data will expand to #{builder.data_size} bytes."