ocran 1.3.18 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.txt +309 -292
  3. data/LICENSE.txt +22 -22
  4. data/README.md +549 -533
  5. data/exe/ocran +5 -5
  6. data/ext/extconf.rb +15 -0
  7. data/lib/ocran/build_constants.rb +16 -16
  8. data/lib/ocran/build_facade.rb +17 -17
  9. data/lib/ocran/build_helper.rb +110 -105
  10. data/lib/ocran/command_output.rb +22 -22
  11. data/lib/ocran/dir_builder.rb +162 -0
  12. data/lib/ocran/direction.rb +636 -458
  13. data/lib/ocran/file_path_set.rb +69 -69
  14. data/lib/ocran/gem_spec_queryable.rb +172 -172
  15. data/lib/ocran/host_config_helper.rb +57 -44
  16. data/lib/ocran/inno_setup_script_builder.rb +111 -111
  17. data/lib/ocran/launcher_batch_builder.rb +85 -85
  18. data/lib/ocran/library_detector.rb +61 -61
  19. data/lib/ocran/library_detector_posix.rb +55 -0
  20. data/lib/ocran/option.rb +323 -273
  21. data/lib/ocran/refine_pathname.rb +104 -104
  22. data/lib/ocran/runner.rb +115 -105
  23. data/lib/ocran/runtime_environment.rb +46 -46
  24. data/lib/ocran/stub_builder.rb +298 -264
  25. data/lib/ocran/version.rb +5 -5
  26. data/lib/ocran/windows_command_escaping.rb +15 -15
  27. data/lib/ocran.rb +7 -7
  28. data/share/ocran/lzma.exe +0 -0
  29. data/src/Makefile +75 -0
  30. data/src/edicon.c +161 -0
  31. data/src/error.c +100 -0
  32. data/src/error.h +66 -0
  33. data/src/inst_dir.c +334 -0
  34. data/src/inst_dir.h +157 -0
  35. data/src/lzma/7zTypes.h +529 -0
  36. data/src/lzma/Compiler.h +43 -0
  37. data/src/lzma/LzmaDec.c +1363 -0
  38. data/src/lzma/LzmaDec.h +236 -0
  39. data/src/lzma/Precomp.h +10 -0
  40. data/src/script_info.c +246 -0
  41. data/src/script_info.h +7 -0
  42. data/src/stub.c +133 -0
  43. data/src/stub.manifest +29 -0
  44. data/src/stub.rc +3 -0
  45. data/src/system_utils.c +1002 -0
  46. data/src/system_utils.h +209 -0
  47. data/src/system_utils_posix.c +500 -0
  48. data/src/unpack.c +574 -0
  49. data/src/unpack.h +85 -0
  50. data/src/vit-ruby.ico +0 -0
  51. metadata +52 -16
  52. data/share/ocran/edicon.exe +0 -0
  53. data/share/ocran/stub.exe +0 -0
  54. data/share/ocran/stubw.exe +0 -0
@@ -1,458 +1,636 @@
1
- # frozen_string_literal: true
2
- require "rbconfig"
3
- require "pathname"
4
- require_relative "refine_pathname"
5
- require_relative "host_config_helper"
6
- require_relative "command_output"
7
- require_relative "build_constants"
8
-
9
- module Ocran
10
- class Direction
11
- using RefinePathname
12
-
13
- # Match the load path against standard library, site_ruby, and vendor_ruby paths
14
- # This regular expression matches:
15
- # - /ruby/3.0.0/
16
- # - /ruby/site_ruby/3.0.0/
17
- # - /ruby/vendor_ruby/3.0.0/
18
- RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+)/?$}i
19
-
20
- include BuildConstants, CommandOutput, HostConfigHelper
21
-
22
- attr_reader :ruby_executable, :rubyopt
23
-
24
- def initialize(post_env, pre_env, option)
25
- @post_env, @pre_env, @option = post_env, pre_env, option
26
- @ruby_executable = @option.windowed? ? rubyw_exe : ruby_exe
27
-
28
- # Initializes @rubyopt with the user-intended RUBYOPT environment variable.
29
- # This ensures that RUBYOPT matches the user's initial settings before any
30
- # modifications that may occur during script execution.
31
- @rubyopt = @option.rubyopt || pre_env.env["RUBYOPT"] || ""
32
-
33
- # FIXME: Remove the absolute path to bundler/setup from RUBYOPT
34
- # This is a temporary measure to ensure compatibility with self-extracting executables
35
- # built in a bundle exec environment, particularly for Ruby 3.2 and later where
36
- # absolute paths are included in RUBYOPT.
37
- # In the future, we plan to implement a more appropriate solution.
38
- @rubyopt = @rubyopt.gsub(%r(-r#{Regexp.escape(RbConfig::TOPDIR)}(/.*/bundler/setup)), "")
39
- end
40
-
41
- # Resolves the common root directory prefix from an array of absolute paths.
42
- # This method iterates over each file path, checking if they have a subpath
43
- # that matches a given execution prefix.
44
- def resolve_root_prefix(files)
45
- files.inject(files.first.dirname) do |current_root, file|
46
- next current_root if file.subpath?(exec_prefix)
47
-
48
- current_root.ascend.find do |candidate_root|
49
- path_from_root = file.relative_path_from(candidate_root)
50
- rescue ArgumentError
51
- raise "No common directory contains all specified files"
52
- else
53
- path_from_root.each_filename.first != ".."
54
- end
55
- end
56
- end
57
-
58
- # For RubyInstaller environments supporting Ruby 2.4 and above,
59
- # this method checks for the existence of a required manifest file
60
- def ruby_builtin_manifest
61
- manifest_path = exec_prefix / "bin/ruby_builtin_dlls/ruby_builtin_dlls.manifest"
62
- manifest_path.exist? ? manifest_path : nil
63
- end
64
-
65
- def detect_dlls
66
- require_relative "library_detector"
67
- LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
68
- end
69
-
70
- def find_gemspecs(features)
71
- require_relative "gem_spec_queryable"
72
-
73
- specs = []
74
- # If a Bundler Gemfile was provided, add all gems it specifies
75
- if @option.gemfile
76
- say "Scanning Gemfile"
77
- specs += GemSpecQueryable.scanning_gemfile(@option.gemfile).each do |spec|
78
- verbose "From Gemfile, adding gem #{spec.full_name}"
79
- end
80
- end
81
- if defined?(Gem)
82
- specs += Gem.loaded_specs.values
83
- # Now, we also detect gems that are not included in Gem.loaded_specs.
84
- # Therefore, we look for any loaded file from a gem path.
85
- specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
86
- end
87
- # Prioritize the spec detected from Gemfile.
88
- specs.uniq!(&:name)
89
- specs
90
- end
91
-
92
- def normalized_features
93
- features = @post_env.loaded_features.map { |feature| Pathname(feature) }
94
-
95
- # Since https://github.com/rubygems/rubygems/commit/cad4cf16cf8fcc637d9da643ef97cf0be2ed63cb
96
- # rubygems/core_ext/kernel_require.rb is evaled and thus missing in $LOADED_FEATURES, so we can't find it and need to add it manually
97
- features.push(Pathname("rubygems/core_ext/kernel_require.rb"))
98
-
99
- # Convert all relative paths to absolute paths before building.
100
- # NOTE: In the future, different strategies may be needed before and after script execution.
101
- features.filter_map do |feature|
102
- if feature.absolute?
103
- feature
104
- elsif (load_path = @post_env.find_load_path(feature))
105
- feature.expand_path(@post_env.expand_path(load_path))
106
- else
107
- # This message occurs when paths for core library files (e.g., enumerator.so,
108
- # rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
109
- # found. These are integral to Ruby's standard libraries or extensions and
110
- # may not be located via normal load path searches, especially in RubyInstaller
111
- # environments.
112
- verbose "Load path not found for #{feature}, skip this feature"
113
- nil
114
- end
115
- end
116
- end
117
-
118
- def construct(builder)
119
- # Store the currently loaded files
120
- features = normalized_features
121
-
122
- say "Building #{@option.output_executable}"
123
- require_relative "build_helper"
124
- builder.extend(BuildHelper)
125
-
126
- # Add the ruby executable and DLL
127
- say "Adding ruby executable #{ruby_executable}"
128
- builder.copy_to_bin(bindir / ruby_executable, ruby_executable)
129
- if libruby_so
130
- builder.copy_to_bin(bindir / libruby_so, libruby_so)
131
- end
132
-
133
- # Add detected DLLs
134
- if @option.auto_detect_dlls?
135
- detect_dlls.each do |dll|
136
- next unless dll.subpath?(exec_prefix) && dll.extname?(".dll") && dll.basename != libruby_so
137
-
138
- say "Adding detected DLL #{dll}"
139
- if dll.subpath?(exec_prefix)
140
- builder.duplicate_to_exec_prefix(dll)
141
- else
142
- builder.copy_to_bin(dll, dll.basename)
143
- end
144
- end
145
- end
146
-
147
- # Add external manifest and builtin DLLs
148
- if (manifest = ruby_builtin_manifest)
149
- manifest.dirname.each_child do |path|
150
- next if path.directory?
151
- say "Adding builtin DLL/manifest #{path}"
152
- builder.duplicate_to_exec_prefix(path)
153
- end
154
- end
155
-
156
- # Include SxS assembly manifests for native extensions.
157
- # Each .so file may have an embedded manifest referencing a companion
158
- # *.so-assembly.manifest file in the same directory. Without these
159
- # manifests the SxS activation context fails (error 14001) at runtime.
160
- # Scan archdir and the extension dirs of all loaded gems.
161
- sxs_manifest_dirs = []
162
- archdir = Pathname(RbConfig::CONFIG["archdir"])
163
- sxs_manifest_dirs << archdir if archdir.exist? && archdir.subpath?(exec_prefix)
164
- if defined?(Gem)
165
- Gem.loaded_specs.each_value do |spec|
166
- next if spec.extensions.empty?
167
- ext_dir = Pathname(spec.extension_dir)
168
- sxs_manifest_dirs << ext_dir if ext_dir.exist? && ext_dir.subpath?(exec_prefix)
169
- end
170
- end
171
- sxs_manifest_dirs.each do |dir|
172
- dir.each_child do |path|
173
- next unless path.extname == ".manifest"
174
- say "Adding native extension assembly manifest #{path}"
175
- builder.duplicate_to_exec_prefix(path)
176
- end
177
- end
178
-
179
- # Add extra DLLs specified on the command line
180
- @option.extra_dlls.each do |dll|
181
- say "Adding supplied DLL #{dll}"
182
- builder.copy_to_bin(bindir / dll, dll)
183
- end
184
-
185
- # Searches for features that are loaded from gems, then produces a
186
- # list of files included in those gems' manifests. Also returns a
187
- # list of original features that caused those gems to be included.
188
- gem_files = find_gemspecs(features).flat_map do |spec|
189
- spec_file = Pathname(spec.loaded_from)
190
- # FIXME: From Ruby 3.2 onwards, launching Ruby with bundle exec causes
191
- # Bundler's loaded_from to point to the root directory of the
192
- # bundler gem, not returning the path to gemspec files. Here, we
193
- # are only collecting gemspec files.
194
- unless spec_file.file?
195
- verbose "Gem #{spec.full_name} root folder was not found, skipping"
196
- next []
197
- end
198
-
199
- # Add gemspec files
200
- if spec_file.subpath?(exec_prefix)
201
- builder.duplicate_to_exec_prefix(spec_file)
202
- elsif (gem_path = GemSpecQueryable.find_gem_path(spec_file))
203
- builder.duplicate_to_gem_home(spec_file, gem_path)
204
- else
205
- raise "Gem spec #{spec_file} does not exist in the Ruby installation. Don't know where to put it."
206
- end
207
-
208
- # Determine which set of files to include for this particular gem
209
- include = GemSpecQueryable.gem_inclusion_set(spec.name, @option.gem_options)
210
- say "Detected gem #{spec.full_name} (#{include.join(", ")})"
211
-
212
- spec.extend(GemSpecQueryable)
213
-
214
- actual_files = spec.find_gem_files(include, features)
215
- say "\t#{actual_files.size} files, #{actual_files.sum(0, &:size)} bytes"
216
-
217
- # Decide where to put gem files, either the system gem folder, or
218
- # GEMDIR.
219
- actual_files.each do |gemfile|
220
- if gemfile.subpath?(exec_prefix)
221
- builder.duplicate_to_exec_prefix(gemfile)
222
- elsif (gem_path = GemSpecQueryable.find_gem_path(gemfile))
223
- builder.duplicate_to_gem_home(gemfile, gem_path)
224
- else
225
- raise "Don't know where to put gemfile #{gemfile}"
226
- end
227
- end
228
-
229
- actual_files
230
- end
231
- gem_files.uniq!
232
-
233
- features -= gem_files
234
-
235
- # If requested, add all ruby standard libraries
236
- if @option.add_all_core?
237
- say "Will include all ruby core libraries"
238
- all_core_dir.each do |path|
239
- # Match the load path against standard library, site_ruby, and vendor_ruby paths
240
- unless (subdir = path.to_posix.match(RUBY_LIBRARY_PATH_REGEX)&.[](1))
241
- raise "Unexpected library path format (does not match core dirs): #{path}"
242
- end
243
- path.find.each do |src|
244
- next if src.directory?
245
- a = Pathname(subdir) / src.relative_path_from(path)
246
- builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
247
- end
248
- end
249
- end
250
-
251
- # Include encoding support files
252
- if @option.add_all_encoding?
253
- @post_env.load_path.each do |load_path|
254
- load_path = Pathname(@post_env.expand_path(load_path))
255
- next unless load_path.subpath?(exec_prefix)
256
-
257
- enc_dir = load_path / "enc"
258
- next unless enc_dir.directory?
259
-
260
- enc_files = enc_dir.find.select { |path| path.file? && path.extname?(".so") }
261
- say "Including #{enc_files.size} encoding support files (#{enc_files.sum(0, &:size)} bytes, use --no-enc to exclude)"
262
- enc_files.each do |path|
263
- builder.duplicate_to_exec_prefix(path)
264
- end
265
- end
266
- else
267
- say "Not including encoding support files"
268
- end
269
-
270
- # Workaround: RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
271
- # Add an empty file to /msys64/usr/bin/msys-2.0.dll if the dll was not required otherwise
272
- builder.touch('msys64/usr/bin/msys-2.0.dll')
273
-
274
- # Find the source root and adjust paths
275
- source_files = @option.source_files.dup
276
- src_prefix = resolve_root_prefix(source_files)
277
-
278
- # Find features and decide where to put them in the temporary
279
- # directory layout.
280
- src_load_path = []
281
- # Add loaded libraries (features, gems)
282
- say "Adding library files"
283
- added_load_paths = (@post_env.load_path - @pre_env.load_path).map { |load_path| Pathname(@post_env.expand_path(load_path)) }
284
- pre_working_directory = Pathname(@pre_env.pwd)
285
- working_directory = Pathname(@post_env.pwd)
286
- features.each do |feature|
287
- load_path = @post_env.find_load_path(feature)
288
- if load_path.nil?
289
- source_files << feature
290
- next
291
- end
292
- abs_load_path = Pathname(@post_env.expand_path(load_path))
293
- if abs_load_path == pre_working_directory
294
- source_files << feature
295
- elsif feature.subpath?(exec_prefix)
296
- # Features found in the Ruby installation are put in the
297
- # temporary Ruby installation.
298
- builder.duplicate_to_exec_prefix(feature)
299
- elsif (gem_path = GemSpecQueryable.find_gem_path(feature))
300
- # Features found in any other Gem path (e.g. ~/.gems) is put
301
- # in a special 'gems' folder.
302
- builder.duplicate_to_gem_home(feature, gem_path)
303
- elsif feature.subpath?(src_prefix) || abs_load_path == working_directory
304
- # Any feature found inside the src_prefix automatically gets
305
- # added as a source file (to go in 'src').
306
- source_files << feature
307
- # Add the load path unless it was added by the script while
308
- # running (or we assume that the script can also set it up
309
- # correctly when running from the resulting executable).
310
- src_load_path << abs_load_path unless added_load_paths.include?(abs_load_path)
311
- elsif added_load_paths.include?(abs_load_path)
312
- # Any feature that exist in a load path added by the script
313
- # itself is added as a file to go into the 'src' (src_prefix
314
- # will be adjusted below to point to the common parent).
315
- source_files << feature
316
- else
317
- # All other feature that can not be resolved go in the the
318
- # Ruby sitelibdir. This is automatically in the load path
319
- # when Ruby starts.
320
- inst_sitelibdir = sitelibdir.relative_path_from(exec_prefix)
321
- builder.cp(feature, inst_sitelibdir / feature.relative_path_from(abs_load_path))
322
- end
323
- end
324
-
325
- # Recompute the src_prefix. Files may have been added implicitly
326
- # while scanning through features.
327
- inst_src_prefix = resolve_root_prefix(source_files)
328
-
329
- # Add explicitly mentioned files
330
- say "Adding user-supplied source files"
331
- source_files.each do |source|
332
- target = builder.resolve_source_path(source, inst_src_prefix)
333
-
334
- if source.directory?
335
- builder.mkdir(target)
336
- else
337
- builder.cp(source, target)
338
- end
339
- end
340
-
341
- # Bundle SSL certificates if OpenSSL was loaded (e.g. via net/http HTTPS)
342
- if defined?(OpenSSL)
343
- cert_file = Pathname(OpenSSL::X509::DEFAULT_CERT_FILE)
344
- if cert_file.file? && cert_file.subpath?(exec_prefix)
345
- say "Adding SSL certificate file #{cert_file}"
346
- builder.duplicate_to_exec_prefix(cert_file)
347
- builder.export("SSL_CERT_FILE", File.join(EXTRACT_ROOT, cert_file.relative_path_from(exec_prefix).to_posix))
348
- end
349
-
350
- cert_dir = Pathname(OpenSSL::X509::DEFAULT_CERT_DIR)
351
- if cert_dir.directory? && cert_dir.subpath?(exec_prefix)
352
- say "Adding SSL certificate directory #{cert_dir}"
353
- cert_dir.find.each do |path|
354
- next if path.directory?
355
- builder.duplicate_to_exec_prefix(path)
356
- end
357
- builder.export("SSL_CERT_DIR", File.join(EXTRACT_ROOT, cert_dir.relative_path_from(exec_prefix).to_posix))
358
- end
359
- end
360
-
361
- # Bundle Tcl/Tk library scripts if the Tk extension is loaded.
362
- # tcl86.dll and tk86.dll are auto-detected by DLL scanning, but the
363
- # Tcl/Tk script libraries (init.tcl etc.) must also be bundled so
364
- # that Tcl can find them relative to the DLL at runtime.
365
- if defined?(TclTkLib)
366
- exec_prefix.glob("**/lib/tcl[0-9]*/init.tcl").each do |init_tcl|
367
- tcl_lib_dir = init_tcl.dirname
368
- next unless tcl_lib_dir.subpath?(exec_prefix)
369
- say "Adding Tcl library files #{tcl_lib_dir}"
370
- tcl_lib_dir.find.each do |path|
371
- next if path.directory?
372
- builder.duplicate_to_exec_prefix(path)
373
- end
374
- end
375
-
376
- exec_prefix.glob("**/lib/tk[0-9]*/pkgIndex.tcl").each do |pkg_index|
377
- tk_lib_dir = pkg_index.dirname
378
- next unless tk_lib_dir.subpath?(exec_prefix)
379
- say "Adding Tk library files #{tk_lib_dir}"
380
- tk_lib_dir.find.each do |path|
381
- next if path.directory?
382
- builder.duplicate_to_exec_prefix(path)
383
- end
384
- end
385
- end
386
-
387
- # Set environment variable
388
- builder.export("RUBYOPT", rubyopt)
389
- # Add the load path that are required with the correct path after
390
- # src_prefix was adjusted.
391
- load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
392
- builder.set_env_path("RUBYLIB", *load_path)
393
- builder.set_env_path("GEM_PATH", GEMDIR)
394
-
395
- # Add the opcode to launch the script
396
- installed_ruby_exe = BINDIR / ruby_executable
397
- target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
398
- builder.exec(installed_ruby_exe, target_script, *@option.argv)
399
- end
400
-
401
- def to_proc
402
- method(:construct).to_proc
403
- end
404
-
405
- def build_inno_setup_installer
406
- require_relative "inno_setup_script_builder"
407
- iss_builder = InnoSetupScriptBuilder.new(@option.inno_setup_script)
408
-
409
- require_relative "launcher_batch_builder"
410
- launcher_builder = LauncherBatchBuilder.new(
411
- chdir_before: @option.chdir_before?,
412
- title: @option.output_executable.basename.sub_ext("")
413
- )
414
-
415
- require_relative "build_facade"
416
- builder = BuildFacade.new(iss_builder, launcher_builder)
417
-
418
- if @option.icon_filename
419
- builder.cp(@option.icon_filename, File.basename(@option.icon_filename))
420
- end
421
-
422
- construct(builder)
423
-
424
- say "Build launcher batch file"
425
- launcher_path = launcher_builder.build
426
- verbose File.read(launcher_path)
427
- builder.cp(launcher_path, "launcher.bat")
428
-
429
- say "Build inno setup script file"
430
- iss_path = iss_builder.build
431
- verbose File.read(iss_path)
432
-
433
- say "Running Inno Setup Command-Line compiler (ISCC)"
434
- iss_builder.compile(verbose: @option.verbose?)
435
-
436
- say "Finished building installer file"
437
- end
438
-
439
- def build_stab_exe
440
- require_relative "stub_builder"
441
-
442
- if @option.enable_debug_mode?
443
- say "Enabling debug mode in executable"
444
- end
445
-
446
- StubBuilder.new(@option.output_executable,
447
- chdir_before: @option.chdir_before?,
448
- debug_extract: @option.enable_debug_extract?,
449
- debug_mode: @option.enable_debug_mode?,
450
- enable_compression: @option.enable_compression?,
451
- gui_mode: @option.windowed?,
452
- icon_path: @option.icon_filename,
453
- &to_proc) => builder
454
- say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
455
- say "After decompression, the data will expand to #{builder.data_size} bytes."
456
- end
457
- end
458
- end
1
+ # frozen_string_literal: true
2
+ require "rbconfig"
3
+ require "pathname"
4
+ require_relative "refine_pathname"
5
+ require_relative "host_config_helper"
6
+ require_relative "command_output"
7
+ require_relative "build_constants"
8
+
9
+ module Ocran
10
+ class Direction
11
+ using RefinePathname
12
+
13
+ # Match the load path against standard library, site_ruby, and vendor_ruby paths
14
+ # This regular expression matches:
15
+ # - /ruby/3.0.0/
16
+ # - /ruby/site_ruby/3.0.0/
17
+ # - /ruby/vendor_ruby/3.0.0/
18
+ RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+)/?$}i
19
+
20
+ include BuildConstants, CommandOutput, HostConfigHelper
21
+
22
+ attr_reader :ruby_executable, :rubyopt
23
+
24
+ def initialize(post_env, pre_env, option)
25
+ @post_env, @pre_env, @option = post_env, pre_env, option
26
+ @ruby_executable = @option.windowed? ? rubyw_exe : ruby_exe
27
+
28
+ # Initializes @rubyopt with the user-intended RUBYOPT environment variable.
29
+ # This ensures that RUBYOPT matches the user's initial settings before any
30
+ # modifications that may occur during script execution.
31
+ @rubyopt = @option.rubyopt || pre_env.env["RUBYOPT"] || ""
32
+
33
+ # Remove any absolute path to bundler/setup from RUBYOPT.
34
+ # When building under `bundle exec`, RUBYOPT contains `-r/absolute/path/bundler/setup`.
35
+ # That path doesn't exist inside the packed executable's environment, causing Ruby to
36
+ # print "RubyGems were not loaded" / "did_you_mean was not loaded" warnings on startup.
37
+ # We strip the flag regardless of install prefix because the gem may live in a user gem
38
+ # directory that doesn't share a prefix with RbConfig::TOPDIR (e.g. on CI runners).
39
+ @rubyopt = @rubyopt.gsub(/-r\S*\/bundler\/setup/, "").strip
40
+ end
41
+
42
+ # Resolves the common root directory prefix from an array of absolute paths.
43
+ # This method iterates over each file path, checking if they have a subpath
44
+ # that matches a given execution prefix.
45
+ def resolve_root_prefix(files)
46
+ files.inject(files.first.dirname) do |current_root, file|
47
+ next current_root if file.subpath?(exec_prefix)
48
+
49
+ current_root.ascend.find do |candidate_root|
50
+ path_from_root = file.relative_path_from(candidate_root)
51
+ rescue ArgumentError
52
+ raise "No common directory contains all specified files"
53
+ else
54
+ path_from_root.each_filename.first != ".."
55
+ end
56
+ end
57
+ end
58
+
59
+ # For RubyInstaller environments supporting Ruby 2.4 and above,
60
+ # this method checks for the existence of a required manifest file
61
+ def ruby_builtin_manifest
62
+ manifest_path = exec_prefix / "bin/ruby_builtin_dlls/ruby_builtin_dlls.manifest"
63
+ manifest_path.exist? ? manifest_path : nil
64
+ end
65
+
66
+ def detect_dlls
67
+ if Gem.win_platform?
68
+ require_relative "library_detector"
69
+ else
70
+ require_relative "library_detector_posix"
71
+ end
72
+ LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
73
+ end
74
+
75
+ def find_gemspecs(features)
76
+ require_relative "gem_spec_queryable"
77
+
78
+ specs = []
79
+ # If a Bundler Gemfile was provided, add all gems it specifies
80
+ if @option.gemfile
81
+ say "Scanning Gemfile"
82
+ specs += GemSpecQueryable.scanning_gemfile(@option.gemfile).each do |spec|
83
+ verbose "From Gemfile, adding gem #{spec.full_name}"
84
+ end
85
+ end
86
+ if defined?(Gem)
87
+ specs += Gem.loaded_specs.values
88
+ # Now, we also detect gems that are not included in Gem.loaded_specs.
89
+ # Therefore, we look for any loaded file from a gem path.
90
+ specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
91
+ end
92
+ # Prioritize the spec detected from Gemfile.
93
+ specs.uniq!(&:name)
94
+ specs
95
+ end
96
+
97
+ def normalized_features
98
+ features = @post_env.loaded_features.map { |feature| Pathname(feature) }
99
+
100
+ # Since https://github.com/rubygems/rubygems/commit/cad4cf16cf8fcc637d9da643ef97cf0be2ed63cb
101
+ # rubygems/core_ext/kernel_require.rb is loaded via IO.read+eval rather than require,
102
+ # so it never appears in $LOADED_FEATURES and must be added manually.
103
+ # We check multiple candidate locations because the layout varies by Ruby setup:
104
+ # - Standard Ruby (including RubyInstaller on Windows): rubygems.rb lives in rubylibdir
105
+ # - Ruby with rubygems-update (e.g. asdf on Linux/macOS): rubygems.rb lives in site_ruby
106
+ # kernel_require.rb must be packed alongside the rubygems.rb that was actually loaded,
107
+ # because rubygems.rb uses require_relative to load it.
108
+ kernel_require_rel = "rubygems/core_ext/kernel_require.rb"
109
+ unless features.any? { |f| f.to_posix.end_with?(kernel_require_rel) }
110
+ # Prefer the location alongside the actually-loaded rubygems.rb, fall back to rubylibdir
111
+ rubygems_feature = features.find { |f| f.to_posix.end_with?("/rubygems.rb") }
112
+ candidate_dirs = []
113
+ candidate_dirs << rubygems_feature.dirname if rubygems_feature
114
+ candidate_dirs << Pathname(RbConfig::CONFIG["rubylibdir"])
115
+ candidate_dirs.each do |base_dir|
116
+ kernel_require_path = base_dir / kernel_require_rel
117
+ if kernel_require_path.exist?
118
+ features.push(kernel_require_path)
119
+ break
120
+ end
121
+ end
122
+ end
123
+
124
+ # Convert all relative paths to absolute paths before building.
125
+ # NOTE: In the future, different strategies may be needed before and after script execution.
126
+ features.filter_map do |feature|
127
+ if feature.absolute?
128
+ feature
129
+ elsif (load_path = @post_env.find_load_path(feature))
130
+ feature.expand_path(@post_env.expand_path(load_path))
131
+ else
132
+ # This message occurs when paths for core library files (e.g., enumerator.so,
133
+ # rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
134
+ # found. These are integral to Ruby's standard libraries or extensions and
135
+ # may not be located via normal load path searches, especially in RubyInstaller
136
+ # environments.
137
+ verbose "Load path not found for #{feature}, skip this feature"
138
+ nil
139
+ end
140
+ end
141
+ end
142
+
143
+ def construct(builder)
144
+ # Store the currently loaded files
145
+ features = normalized_features
146
+
147
+ say "Building #{@option.output_executable}"
148
+ require_relative "build_helper"
149
+ builder.extend(BuildHelper)
150
+
151
+ # Add the ruby executable and DLL
152
+ say "Adding ruby executable #{ruby_executable}"
153
+ builder.copy_to_bin(bindir / ruby_executable, ruby_executable)
154
+ if libruby_so
155
+ # On POSIX systems, libruby.so is in libdir; on Windows, it's in bindir
156
+ libruby_src = Gem.win_platform? ? bindir / libruby_so : libdir / libruby_so
157
+ builder.copy_to_bin(libruby_src, libruby_so)
158
+
159
+ # On POSIX systems, create symlinks (aliases) for libruby.so
160
+ unless Gem.win_platform?
161
+ libruby_aliases.each do |libruby_alias|
162
+ builder.symlink_in_bin(libruby_so, libruby_alias)
163
+ end
164
+ end
165
+ end
166
+
167
+ # On POSIX systems, set LD_LIBRARY_PATH to find bundled shared libraries
168
+ unless Gem.win_platform?
169
+ extract_bin = File.join(EXTRACT_ROOT, BINDIR.to_s)
170
+ builder.export("LD_LIBRARY_PATH", extract_bin)
171
+ if RUBY_PLATFORM.include?("darwin")
172
+ builder.export("DYLD_LIBRARY_PATH", extract_bin)
173
+ end
174
+ end
175
+
176
+ # Windows-only: Add detected DLLs
177
+ if Gem.win_platform? && @option.auto_detect_dlls?
178
+ detect_dlls.each do |dll|
179
+ next unless dll.subpath?(exec_prefix) && dll.extname?(".dll") && dll.basename != libruby_so
180
+
181
+ say "Adding detected DLL #{dll}"
182
+ if dll.subpath?(exec_prefix)
183
+ builder.duplicate_to_exec_prefix(dll)
184
+ else
185
+ builder.copy_to_bin(dll, dll.basename)
186
+ end
187
+ end
188
+ end
189
+
190
+ # Windows-only: Add external manifest and builtin DLLs
191
+ if Gem.win_platform?
192
+ if (manifest = ruby_builtin_manifest)
193
+ manifest.dirname.each_child do |path|
194
+ next if path.directory?
195
+ say "Adding builtin DLL/manifest #{path}"
196
+ builder.duplicate_to_exec_prefix(path)
197
+ end
198
+ end
199
+
200
+ # Include SxS assembly manifests for native extensions.
201
+ # Each .so file may have an embedded manifest referencing a companion
202
+ # *.so-assembly.manifest file in the same directory. Without these
203
+ # manifests the SxS activation context fails (error 14001) at runtime.
204
+ # Scan archdir and the extension dirs of all loaded gems.
205
+ sxs_manifest_dirs = []
206
+ archdir = Pathname(RbConfig::CONFIG["archdir"])
207
+ sxs_manifest_dirs << archdir if archdir.exist? && archdir.subpath?(exec_prefix)
208
+ if defined?(Gem)
209
+ Gem.loaded_specs.each_value do |spec|
210
+ next if spec.extensions.empty?
211
+ ext_dir = Pathname(spec.extension_dir)
212
+ sxs_manifest_dirs << ext_dir if ext_dir.exist? && ext_dir.subpath?(exec_prefix)
213
+ end
214
+ end
215
+ sxs_manifest_dirs.each do |dir|
216
+ dir.each_child do |path|
217
+ next unless path.extname == ".manifest"
218
+ say "Adding native extension assembly manifest #{path}"
219
+ builder.duplicate_to_exec_prefix(path)
220
+ end
221
+ end
222
+
223
+ # Add extra DLLs specified on the command line
224
+ @option.extra_dlls.each do |dll|
225
+ say "Adding supplied DLL #{dll}"
226
+ builder.copy_to_bin(bindir / dll, dll)
227
+ end
228
+ end
229
+
230
+ # Searches for features that are loaded from gems, then produces a
231
+ # list of files included in those gems' manifests. Also returns a
232
+ # list of original features that caused those gems to be included.
233
+ gem_files = find_gemspecs(features).flat_map do |spec|
234
+ spec_file = Pathname(spec.loaded_from)
235
+ # FIXME: From Ruby 3.2 onwards, launching Ruby with bundle exec causes
236
+ # Bundler's loaded_from to point to the root directory of the
237
+ # bundler gem, not returning the path to gemspec files. Here, we
238
+ # are only collecting gemspec files.
239
+ unless spec_file.file?
240
+ verbose "Gem #{spec.full_name} root folder was not found, skipping"
241
+ next []
242
+ end
243
+
244
+ # Add gemspec files
245
+ if spec_file.subpath?(exec_prefix)
246
+ builder.duplicate_to_exec_prefix(spec_file)
247
+ elsif (gem_path = GemSpecQueryable.find_gem_path(spec_file))
248
+ builder.duplicate_to_gem_home(spec_file, gem_path)
249
+ else
250
+ raise "Gem spec #{spec_file} does not exist in the Ruby installation. Don't know where to put it."
251
+ end
252
+
253
+ # Determine which set of files to include for this particular gem
254
+ include = GemSpecQueryable.gem_inclusion_set(spec.name, @option.gem_options)
255
+ say "Detected gem #{spec.full_name} (#{include.join(", ")})"
256
+
257
+ spec.extend(GemSpecQueryable)
258
+
259
+ verbose "\tgem_dir: #{spec.gem_dir}"
260
+ verbose "\tgem_dir exists: #{File.directory?(spec.gem_dir)}"
261
+ loaded_matches = include.include?(:loaded) ? features.select { |f| f.subpath?(spec.gem_dir) } : []
262
+ verbose "\t:loaded candidates in features: #{loaded_matches.size}"
263
+ loaded_matches.each { |f| verbose "\t loaded: #{f}" }
264
+ resource_count = include.include?(:files) && File.directory?(spec.gem_dir) ? spec.resource_files.size : 0
265
+ verbose "\t:files (resource_files) count: #{resource_count}"
266
+
267
+ actual_files = spec.find_gem_files(include, features)
268
+ say "\t#{actual_files.size} files, #{actual_files.sum(0, &:size)} bytes"
269
+
270
+ # Decide where to put gem files, either the system gem folder, or
271
+ # GEMDIR.
272
+ actual_files.each do |gemfile|
273
+ if gemfile.subpath?(exec_prefix)
274
+ builder.duplicate_to_exec_prefix(gemfile)
275
+ elsif (gem_path = GemSpecQueryable.find_gem_path(gemfile))
276
+ builder.duplicate_to_gem_home(gemfile, gem_path)
277
+ else
278
+ raise "Don't know where to put gemfile #{gemfile}"
279
+ end
280
+ end
281
+
282
+ actual_files
283
+ end
284
+ gem_files.uniq!
285
+
286
+ features -= gem_files
287
+
288
+ # If requested, add all ruby standard libraries
289
+ if @option.add_all_core?
290
+ say "Will include all ruby core libraries"
291
+ all_core_dir.each do |path|
292
+ # Match the load path against standard library, site_ruby, and vendor_ruby paths
293
+ unless (subdir = path.to_posix.match(RUBY_LIBRARY_PATH_REGEX)&.[](1))
294
+ raise "Unexpected library path format (does not match core dirs): #{path}"
295
+ end
296
+ path.find.each do |src|
297
+ next if src.directory?
298
+ a = Pathname(subdir) / src.relative_path_from(path)
299
+ builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
300
+ end
301
+ end
302
+ end
303
+
304
+ # Include encoding support files
305
+ if @option.add_all_encoding?
306
+ @post_env.load_path.each do |load_path|
307
+ load_path = Pathname(@post_env.expand_path(load_path))
308
+ next unless load_path.subpath?(exec_prefix)
309
+
310
+ enc_dir = load_path / "enc"
311
+ next unless enc_dir.directory?
312
+
313
+ enc_files = enc_dir.find.select { |path| path.file? && path.extname?(".so") }
314
+ say "Including #{enc_files.size} encoding support files (#{enc_files.sum(0, &:size)} bytes, use --no-enc to exclude)"
315
+ enc_files.each do |path|
316
+ builder.duplicate_to_exec_prefix(path)
317
+ end
318
+ end
319
+ else
320
+ say "Not including encoding support files"
321
+ end
322
+
323
+ # Windows-only: Workaround for RubyInstaller MSYS folder detection
324
+ if Gem.win_platform?
325
+ # RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present
326
+ # (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
327
+ builder.touch('msys64/usr/bin/msys-2.0.dll')
328
+ end
329
+
330
+ # Find the source root and adjust paths
331
+ source_files = @option.source_files.dup
332
+ src_prefix = resolve_root_prefix(source_files)
333
+
334
+ # Find features and decide where to put them in the temporary
335
+ # directory layout.
336
+ src_load_path = []
337
+ # Add loaded libraries (features, gems)
338
+ say "Adding library files"
339
+ added_load_paths = (@post_env.load_path - @pre_env.load_path).map { |load_path| Pathname(@post_env.expand_path(load_path)) }
340
+ pre_working_directory = Pathname(@pre_env.pwd)
341
+ working_directory = Pathname(@post_env.pwd)
342
+ features.each do |feature|
343
+ load_path = @post_env.find_load_path(feature)
344
+ if load_path.nil?
345
+ verbose "\tlibfile: #{feature} -> src (no load path)"
346
+ source_files << feature
347
+ next
348
+ end
349
+ abs_load_path = Pathname(@post_env.expand_path(load_path))
350
+ if abs_load_path == pre_working_directory
351
+ verbose "\tlibfile: #{feature} -> src (pre-working-dir load path)"
352
+ source_files << feature
353
+ elsif feature.subpath?(exec_prefix)
354
+ # Features found in the Ruby installation are put in the
355
+ # temporary Ruby installation.
356
+ verbose "\tlibfile: #{feature} -> exec_prefix"
357
+ builder.duplicate_to_exec_prefix(feature)
358
+ elsif (gem_path = GemSpecQueryable.find_gem_path(feature))
359
+ # Features found in any other Gem path (e.g. ~/.gems) is put
360
+ # in a special 'gems' folder.
361
+ verbose "\tlibfile: #{feature} -> gem_home"
362
+ builder.duplicate_to_gem_home(feature, gem_path)
363
+ elsif feature.subpath?(src_prefix) || abs_load_path == working_directory
364
+ # Any feature found inside the src_prefix automatically gets
365
+ # added as a source file (to go in 'src').
366
+ verbose "\tlibfile: #{feature} -> src (src_prefix/working_dir)"
367
+ source_files << feature
368
+ # Add the load path unless it was added by the script while
369
+ # running (or we assume that the script can also set it up
370
+ # correctly when running from the resulting executable).
371
+ src_load_path << abs_load_path unless added_load_paths.include?(abs_load_path)
372
+ elsif added_load_paths.include?(abs_load_path)
373
+ # Any feature that exist in a load path added by the script
374
+ # itself is added as a file to go into the 'src' (src_prefix
375
+ # will be adjusted below to point to the common parent).
376
+ verbose "\tlibfile: #{feature} -> src (script-added load path)"
377
+ source_files << feature
378
+ else
379
+ # All other feature that can not be resolved go in the the
380
+ # Ruby sitelibdir. This is automatically in the load path
381
+ # when Ruby starts on Windows.
382
+ # On POSIX systems the ruby binary has a compile-time prefix so the
383
+ # extraction dir's sitelibdir is not on the load path; put
384
+ # the file in src instead and add the load path to RUBYLIB.
385
+ if Gem.win_platform?
386
+ inst_sitelibdir = sitelibdir.relative_path_from(exec_prefix)
387
+ builder.cp(feature, inst_sitelibdir / feature.relative_path_from(abs_load_path))
388
+ else
389
+ source_files << feature
390
+ src_load_path << abs_load_path unless src_load_path.include?(abs_load_path)
391
+ end
392
+ end
393
+ end
394
+
395
+ # Recompute the src_prefix. Files may have been added implicitly
396
+ # while scanning through features.
397
+ inst_src_prefix = resolve_root_prefix(source_files)
398
+
399
+ # Add explicitly mentioned files
400
+ say "Adding user-supplied source files"
401
+ source_files.each do |source|
402
+ target = builder.resolve_source_path(source, inst_src_prefix)
403
+
404
+ if source.directory?
405
+ builder.mkdir(target)
406
+ else
407
+ builder.cp(source, target)
408
+ end
409
+ end
410
+
411
+ # Bundle SSL certificates if OpenSSL was loaded (e.g. via net/http HTTPS)
412
+ if defined?(OpenSSL)
413
+ cert_file = Pathname(OpenSSL::X509::DEFAULT_CERT_FILE)
414
+ if cert_file.file? && cert_file.subpath?(exec_prefix)
415
+ say "Adding SSL certificate file #{cert_file}"
416
+ builder.duplicate_to_exec_prefix(cert_file)
417
+ builder.export("SSL_CERT_FILE", File.join(EXTRACT_ROOT, cert_file.relative_path_from(exec_prefix).to_posix))
418
+ end
419
+
420
+ cert_dir = Pathname(OpenSSL::X509::DEFAULT_CERT_DIR)
421
+ if cert_dir.directory? && cert_dir.subpath?(exec_prefix)
422
+ say "Adding SSL certificate directory #{cert_dir}"
423
+ cert_dir.find.each do |path|
424
+ next if path.directory?
425
+ builder.duplicate_to_exec_prefix(path)
426
+ end
427
+ builder.export("SSL_CERT_DIR", File.join(EXTRACT_ROOT, cert_dir.relative_path_from(exec_prefix).to_posix))
428
+ end
429
+ end
430
+
431
+ # Bundle Tcl/Tk library scripts if the Tk extension is loaded.
432
+ # tcl86.dll and tk86.dll are auto-detected by DLL scanning, but the
433
+ # Tcl/Tk script libraries (init.tcl etc.) must also be bundled so
434
+ # that Tcl can find them relative to the DLL at runtime.
435
+ if defined?(TclTkLib)
436
+ exec_prefix.glob("**/lib/tcl[0-9]*/init.tcl").each do |init_tcl|
437
+ tcl_lib_dir = init_tcl.dirname
438
+ next unless tcl_lib_dir.subpath?(exec_prefix)
439
+ say "Adding Tcl library files #{tcl_lib_dir}"
440
+ tcl_lib_dir.find.each do |path|
441
+ next if path.directory?
442
+ builder.duplicate_to_exec_prefix(path)
443
+ end
444
+ end
445
+
446
+ exec_prefix.glob("**/lib/tk[0-9]*/pkgIndex.tcl").each do |pkg_index|
447
+ tk_lib_dir = pkg_index.dirname
448
+ next unless tk_lib_dir.subpath?(exec_prefix)
449
+ say "Adding Tk library files #{tk_lib_dir}"
450
+ tk_lib_dir.find.each do |path|
451
+ next if path.directory?
452
+ builder.duplicate_to_exec_prefix(path)
453
+ end
454
+ end
455
+ end
456
+
457
+ # Set environment variable
458
+ builder.export("RUBYOPT", rubyopt)
459
+ # Add the load path that are required with the correct path after
460
+ # src_prefix was adjusted.
461
+ load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
462
+
463
+ # On POSIX systems, also add the packed Ruby standard library directories
464
+ # to RUBYLIB. The Ruby binary has a compiled-in prefix pointing to the build
465
+ # host, which doesn't exist on other systems (e.g., Docker with no Ruby).
466
+ # By adding the extract-dir equivalents of rubylibdir, sitelibdir, etc. to
467
+ # RUBYLIB, Ruby can find rubygems and the standard library in the packed tree.
468
+ unless Gem.win_platform?
469
+ core_lib_paths = all_core_dir
470
+ .select { |dir| dir.subpath?(exec_prefix) }
471
+ .map { |dir| dir.relative_path_from(exec_prefix) }
472
+ archdir = Pathname(RbConfig::CONFIG["archdir"])
473
+ if archdir.subpath?(exec_prefix)
474
+ core_lib_paths << archdir.relative_path_from(exec_prefix)
475
+ end
476
+ load_path = core_lib_paths + load_path
477
+ end
478
+
479
+ builder.set_env_path("RUBYLIB", *load_path)
480
+ builder.set_env_path("GEM_HOME", GEMDIR)
481
+
482
+ gem_paths = [GEMDIR]
483
+ # On POSIX, default gems (e.g. error_highlight) are stored under the Ruby
484
+ # installation's gem dir (Gem.default_dir), not in GEMDIR. Include it in
485
+ # GEM_PATH so RubyGems can find and activate them in the extracted tree.
486
+ unless Gem.win_platform?
487
+ default_gem_dir = Pathname(Gem.default_dir)
488
+ if default_gem_dir.subpath?(exec_prefix)
489
+ gem_paths << default_gem_dir.relative_path_from(exec_prefix)
490
+ end
491
+ end
492
+ builder.set_env_path("GEM_PATH", *gem_paths)
493
+
494
+ # Add the opcode to launch the script
495
+ installed_ruby_exe = BINDIR / ruby_executable
496
+ target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
497
+ builder.exec(installed_ruby_exe, target_script, *@option.argv)
498
+ end
499
+
500
+ def to_proc
501
+ method(:construct).to_proc
502
+ end
503
+
504
+ def build_inno_setup_installer
505
+ require_relative "inno_setup_script_builder"
506
+ iss_builder = InnoSetupScriptBuilder.new(@option.inno_setup_script)
507
+
508
+ require_relative "launcher_batch_builder"
509
+ launcher_builder = LauncherBatchBuilder.new(
510
+ chdir_before: @option.chdir_before?,
511
+ title: @option.output_executable.basename.sub_ext("")
512
+ )
513
+
514
+ require_relative "build_facade"
515
+ builder = BuildFacade.new(iss_builder, launcher_builder)
516
+
517
+ if @option.icon_filename
518
+ builder.cp(@option.icon_filename, File.basename(@option.icon_filename))
519
+ end
520
+
521
+ construct(builder)
522
+
523
+ say "Build launcher batch file"
524
+ launcher_path = launcher_builder.build
525
+ verbose File.read(launcher_path)
526
+ builder.cp(launcher_path, "launcher.bat")
527
+
528
+ say "Build inno setup script file"
529
+ iss_path = iss_builder.build
530
+ verbose File.read(iss_path)
531
+
532
+ say "Running Inno Setup Command-Line compiler (ISCC)"
533
+ iss_builder.compile(verbose: @option.verbose?)
534
+
535
+ say "Finished building installer file"
536
+ end
537
+
538
+ def build_output_dir(path)
539
+ require_relative "dir_builder"
540
+
541
+ path = Pathname(path)
542
+ say "Building directory #{path}"
543
+ DirBuilder.new(path, &to_proc)
544
+ say "Finished building directory #{path}"
545
+ end
546
+
547
+ def build_zip(path)
548
+ require_relative "dir_builder"
549
+ require "tmpdir"
550
+
551
+ path = Pathname(path)
552
+ say "Building zip #{path}"
553
+ Dir.mktmpdir("ocran") do |tmpdir|
554
+ build_output_dir(tmpdir)
555
+ DirBuilder.create_zip(path, tmpdir)
556
+ end
557
+ say "Finished building #{path} (#{File.size(path)} bytes)"
558
+ end
559
+
560
+ def build_macosx_bundle(bundle_path)
561
+ require_relative "stub_builder"
562
+ require "fileutils"
563
+
564
+ bundle_path = Pathname(bundle_path)
565
+ app_name = bundle_path.basename.sub_ext("").to_s
566
+ contents_dir = bundle_path / "Contents"
567
+ macos_dir = contents_dir / "MacOS"
568
+ resources_dir = contents_dir / "Resources"
569
+
570
+ FileUtils.mkdir_p(macos_dir.to_s)
571
+
572
+ executable_path = macos_dir / app_name
573
+ say "Building app bundle #{bundle_path}"
574
+
575
+ StubBuilder.new(executable_path,
576
+ chdir_before: @option.chdir_before?,
577
+ debug_extract: @option.enable_debug_extract?,
578
+ debug_mode: @option.enable_debug_mode?,
579
+ enable_compression: @option.enable_compression?,
580
+ gui_mode: false,
581
+ icon_path: nil,
582
+ &to_proc) => builder
583
+
584
+ if @option.icon_filename
585
+ FileUtils.mkdir_p(resources_dir.to_s)
586
+ icon_dest = resources_dir / "AppIcon#{@option.icon_filename.extname}"
587
+ FileUtils.cp(@option.icon_filename.to_s, icon_dest.to_s)
588
+ end
589
+
590
+ bundle_id = @option.bundle_identifier || "com.example.#{app_name}"
591
+ icon_entry = @option.icon_filename ? " <key>CFBundleIconFile</key>\n <string>AppIcon</string>\n" : ""
592
+
593
+ File.write(contents_dir / "Info.plist", <<~PLIST)
594
+ <?xml version="1.0" encoding="UTF-8"?>
595
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
596
+ <plist version="1.0">
597
+ <dict>
598
+ <key>CFBundleName</key>
599
+ <string>#{app_name}</string>
600
+ <key>CFBundleDisplayName</key>
601
+ <string>#{app_name}</string>
602
+ <key>CFBundleIdentifier</key>
603
+ <string>#{bundle_id}</string>
604
+ <key>CFBundleVersion</key>
605
+ <string>1.0</string>
606
+ <key>CFBundlePackageType</key>
607
+ <string>APPL</string>
608
+ <key>CFBundleExecutable</key>
609
+ <string>#{app_name}</string>
610
+ #{icon_entry}</dict>
611
+ </plist>
612
+ PLIST
613
+
614
+ say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
615
+ end
616
+
617
+ def build_stab_exe
618
+ require_relative "stub_builder"
619
+
620
+ if @option.enable_debug_mode?
621
+ say "Enabling debug mode in executable"
622
+ end
623
+
624
+ StubBuilder.new(@option.output_executable,
625
+ chdir_before: @option.chdir_before?,
626
+ debug_extract: @option.enable_debug_extract?,
627
+ debug_mode: @option.enable_debug_mode?,
628
+ enable_compression: @option.enable_compression?,
629
+ gui_mode: @option.windowed?,
630
+ icon_path: @option.icon_filename,
631
+ &to_proc) => builder
632
+ say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
633
+ say "After decompression, the data will expand to #{builder.data_size} bytes."
634
+ end
635
+ end
636
+ end