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.
data/lib/ocran/option.rb CHANGED
@@ -16,6 +16,10 @@ module Ocran
16
16
  :macosx_bundle => nil,
17
17
  :macosx_bundle? => false,
18
18
  :chdir_before? => false,
19
+ :chdir_exe_dir? => false,
20
+ :cosmo_cc => nil,
21
+ :cosmo_ruby => nil,
22
+ :cosmo_zip? => false,
19
23
  :enable_compression? => true,
20
24
  :enable_debug_extract? => false,
21
25
  :enable_debug_mode? => false,
@@ -103,13 +107,62 @@ Executable options:
103
107
  --windows Force Windows application (rubyw.exe)
104
108
  --console Force console application (ruby.exe)
105
109
  --chdir-first When exe starts, change working directory to app dir.
110
+ --chdir-exe-dir When exe starts, change working directory to the directory
111
+ containing the executable itself.
106
112
  --icon <ico> Replace icon with a custom one.
107
- --rubyopt <str> Set the RUBYOPT environment variable when running the executable
113
+ --rubyopt <str> Set the RUBYOPT environment variable when running the executable.
114
+ -I/-r entries with absolute build-machine paths are
115
+ translated to their packed locations at build time.
108
116
  --debug Executable will be verbose.
109
117
  --debug-extract Executable will unpack to local dir and not delete after.
118
+
119
+ Experimental options:
120
+
121
+ --cosmo-ruby <ruby.com> Package the given cosmopolitan-built Ruby (an APE,
122
+ e.g. ruby.com) as the interpreter instead of the host
123
+ Ruby: the result is a single <scriptname>.com that runs
124
+ on Linux, Windows and macOS. The APE must be fully
125
+ self-contained (stdlib embedded in its ZIP store).
126
+ Dependency detection still runs under the host Ruby;
127
+ native-extension gems are rejected (pure-Ruby gems are
128
+ packed as usual).
129
+ When the given interpreter runs an embedded /zip/main.rb
130
+ (CosmoRuby builds do), the application is injected into
131
+ its ZIP store: no compiler is needed and the executable
132
+ starts without unpacking anything. Otherwise OCRAN falls
133
+ back to an APE launcher stub that extracts the
134
+ application at every start, and locates the cosmocc
135
+ toolchain to build that stub automatically (COSMOCC
136
+ environment variable, cosmocc in PATH, then conventional
137
+ install locations such as ~/.cosmocc/*/bin/cosmocc).
138
+ --cosmo <path> Use the Cosmopolitan toolchain (cosmocc) at <path> to
139
+ build the launcher stub, overriding the automatic
140
+ toolchain lookup. <path> is the cosmocc executable or
141
+ its install dir. Given together with --cosmo-ruby it
142
+ forces the launcher-stub build even when the interpreter
143
+ supports ZIP packaging. Given without --cosmo-ruby, it
144
+ packages the host Ruby behind an APE stub. Console-only;
145
+ output defaults to <scriptname>.com.
146
+ (Alias: --cosmo-toolchain)
110
147
  EOF
111
148
  end
112
149
 
150
+ # Pulls in Ocran::CosmoToolchain for the --cosmo/--cosmo-ruby options.
151
+ #
152
+ # Deliberately Kernel#load and not require_relative: OCRAN detects the
153
+ # application's dependencies by diffing $LOADED_FEATURES around the
154
+ # dependency run, and the "before" snapshot is taken *before* the command
155
+ # line is parsed (Runner#initialize). Anything require'd from here would
156
+ # therefore show up in that diff and be packed into the user's
157
+ # application - packing OCRAN's own source file with it, and (because the
158
+ # src prefix is the common parent of all source files) moving the whole
159
+ # application down into a deeper src/ subdirectory. Kernel#load leaves
160
+ # $LOADED_FEATURES untouched; the same idiom is used for
161
+ # refine_pathname.rb above.
162
+ def load_cosmo_toolchain
163
+ load File.expand_path("cosmo_toolchain.rb", __dir__) unless defined? CosmoToolchain
164
+ end
165
+
113
166
  def parse(argv)
114
167
  while (arg = argv.shift)
115
168
  case arg
@@ -149,12 +202,22 @@ EOF
149
202
  @options[:load_autoload?] = false
150
203
  when "--chdir-first"
151
204
  @options[:chdir_before?] = true
205
+ when "--chdir-exe-dir"
206
+ @options[:chdir_exe_dir?] = true
152
207
  when "--icon"
153
208
  path = argv.shift
154
209
  raise "Icon file #{path} not found" unless path && File.exist?(path)
155
210
  @options[:icon_filename] = Pathname.new(path).expand_path
156
211
  when "--rubyopt"
157
212
  @options[:rubyopt] = argv.shift
213
+ when "--cosmo", "--cosmo-toolchain"
214
+ # Kept unresolved until validation: resolving here would report
215
+ # "cosmocc ... is not executable" on a Windows build host, ahead of
216
+ # the clearer "not supported when building on Windows" check.
217
+ @options[:cosmo_cc] = argv.shift
218
+ when "--cosmo-ruby"
219
+ load_cosmo_toolchain
220
+ @options[:cosmo_ruby] = CosmoToolchain.resolve_ruby(argv.shift)
158
221
  when "--gemfile"
159
222
  path = argv.shift
160
223
  raise "Gemfile #{path} not found" unless path && File.exist?(path)
@@ -214,13 +277,28 @@ EOF
214
277
 
215
278
  @options[:output_executable] =
216
279
  if output_override
217
- output_override
280
+ # Windows runs a program only by its extension, so a name given
281
+ # without .exe - `--output myapp`, as a build script shared with
282
+ # Linux and macOS writes it - gets one there. .exe and .com names
283
+ # are used as given.
284
+ if Gem.win_platform? && !output_override.extname?(".exe") && !output_override.extname?(".com")
285
+ Pathname("#{output_override}.exe")
286
+ else
287
+ output_override
288
+ end
218
289
  else
219
290
  executable = script
220
291
  # If debug mode is enabled, append "-debug" to the filename
221
292
  executable = executable.append_to_filename("-debug") if enable_debug_mode?
222
- # Build output files are created in the current directory
223
- ext = Gem.win_platform? ? ".exe" : ""
293
+ # Build output files are created in the current directory.
294
+ # APE binaries built with cosmocc conventionally use .com.
295
+ ext = if cosmo?
296
+ ".com"
297
+ elsif Gem.win_platform?
298
+ ".exe"
299
+ else
300
+ ""
301
+ end
224
302
  executable.basename.sub_ext(ext).expand_path
225
303
  end
226
304
 
@@ -229,6 +307,10 @@ EOF
229
307
  @options[:macosx_bundle] = Pathname(bundle_base).sub_ext(".app").expand_path
230
308
  end
231
309
 
310
+ if chdir_before? && chdir_exe_dir?
311
+ raise "--chdir-first and --chdir-exe-dir cannot be used together"
312
+ end
313
+
232
314
  @options[:use_inno_setup?] = !!inno_setup_script
233
315
 
234
316
  @options[:verbose?] &&= !quiet?
@@ -253,6 +335,41 @@ EOF
253
335
  raise "--output-dir and --output-zip cannot be used together"
254
336
  end
255
337
 
338
+ if cosmo?
339
+ opt = cosmo_cc ? "--cosmo" : "--cosmo-ruby"
340
+
341
+ if Gem.win_platform?
342
+ raise "#{opt} is not supported when building on Windows (build the APE package on a Linux/macOS host)"
343
+ end
344
+
345
+ if force_windows?
346
+ raise "--windows cannot be used with #{opt}: cosmocc has no GUI (stubw) equivalent; APE stubs are console-only"
347
+ end
348
+
349
+ load_cosmo_toolchain
350
+
351
+ # A cosmopolitan Ruby that runs an embedded /zip/main.rb can carry
352
+ # the application inside its own ZIP store, which needs no compiler
353
+ # and no launcher stub at all - so that is what --cosmo-ruby does
354
+ # on its own. Naming a toolchain with --cosmo is the way to ask for
355
+ # the launcher stub explicitly; the other output formats
356
+ # (--output-dir, --output-zip, --innosetup, --macosx-bundle) are
357
+ # directory layouts rather than a single binary, so they keep using
358
+ # it too.
359
+ @options[:cosmo_zip?] =
360
+ !!cosmo_ruby && !cosmo_cc && !single_binary_output_conflict &&
361
+ CosmoToolchain.zip_main_support?(cosmo_ruby)
362
+
363
+ unless cosmo_zip?
364
+ # The APE stub is compiled with cosmocc. --cosmo names that
365
+ # toolchain explicitly; otherwise it is discovered on the build
366
+ # host (COSMOCC, PATH, conventional install locations), so one
367
+ # option is still enough. Resolved here, before the dependency
368
+ # run, so a missing toolchain fails fast.
369
+ @options[:cosmo_cc] = CosmoToolchain.require_cc(cosmo_cc)
370
+ end
371
+ end
372
+
256
373
  if macosx_bundle && (output_dir || output_zip || inno_setup_script)
257
374
  raise "--macosx-bundle cannot be combined with --output-dir, --output-zip, or --innosetup"
258
375
  end
@@ -272,6 +389,29 @@ EOF
272
389
 
273
390
  def chdir_before? = @options[__method__]
274
391
 
392
+ def chdir_exe_dir? = @options[__method__]
393
+ def cosmo_cc = @options[__method__]
394
+
395
+ def cosmo_ruby = @options[__method__]
396
+
397
+ # True when the application is packaged by injecting it into the ZIP
398
+ # store of the cosmopolitan Ruby itself: no cosmocc, no launcher stub,
399
+ # and no extraction to a temporary directory at run time.
400
+ def cosmo_zip? = @options[__method__]
401
+
402
+ # Output formats that are not a single self-contained binary, and
403
+ # therefore cannot be produced by injecting into the interpreter.
404
+ def single_binary_output_conflict
405
+ output_dir || output_zip || inno_setup_script || @options[:macosx_bundle?]
406
+ end
407
+ private :single_binary_output_conflict
408
+
409
+ # True when the build produces an APE (Actually Portable Executable),
410
+ # i.e. an APE launcher stub is used: an explicit toolchain (--cosmo), a
411
+ # cosmopolitan Ruby payload (--cosmo-ruby, whose toolchain is inferred),
412
+ # or both.
413
+ def cosmo? = !!(cosmo_cc || cosmo_ruby)
414
+
275
415
  def enable_compression? = @options[__method__]
276
416
 
277
417
  def enable_debug_extract? = @options[__method__]
@@ -314,6 +454,36 @@ EOF
314
454
 
315
455
  def script = @options[__method__]
316
456
 
457
+ # The names Bundler accepts for a Gemfile, most common first.
458
+ GEMFILE_NAMES = %w[Gemfile gems.rb].freeze
459
+ private_constant :GEMFILE_NAMES
460
+
461
+ # The Gemfile the application runs under: the one named by --gemfile,
462
+ # or else the nearest Gemfile at or above the script's directory, which
463
+ # is where Bundler itself would look. Returns nil when the application
464
+ # has no Gemfile at all.
465
+ #
466
+ # This is how the application's own bundle is told apart from whatever
467
+ # bundle OCRAN happens to have been started under. Running
468
+ # `bundle exec ocran app.rb` from the application's own directory is the
469
+ # ordinary case, and there the two are the same file; packaging some
470
+ # other project from inside this one's bundle is not.
471
+ def application_gemfile
472
+ return gemfile if gemfile
473
+
474
+ dir = script.expand_path.dirname
475
+ loop do
476
+ GEMFILE_NAMES.each do |name|
477
+ candidate = dir + name
478
+ return candidate if candidate.file?
479
+ end
480
+ parent = dir.parent
481
+ return nil if parent == dir
482
+
483
+ dir = parent
484
+ end
485
+ end
486
+
317
487
  def source_files = @options[__method__]
318
488
 
319
489
  def use_inno_setup? = @options[__method__]
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ # Parses the RUBYOPT environment variable the way Ruby itself does
5
+ # (whitespace-separated words; -I and -r may take their argument either
6
+ # attached or as the following word) and translates -I/-r entries that
7
+ # refer to absolute build-machine paths into their locations inside the
8
+ # packed application.
9
+ #
10
+ # Translated entries cannot simply be rewritten inside RUBYOPT: the
11
+ # extraction directory is only known at runtime and may contain spaces,
12
+ # and Ruby does not support any form of quoting in RUBYOPT. The
13
+ # translated entries are therefore removed from RUBYOPT and returned
14
+ # separately so that the build can apply them through a generated
15
+ # launcher script instead. See GitHub issue #20.
16
+ class RubyoptProcessor
17
+ # -r entries whose feature path matches this pattern are always removed.
18
+ # When building under `bundle exec` (Ruby 3.2+), Bundler injects
19
+ # `-r<absolute path>/bundler/setup` into RUBYOPT; requiring it inside
20
+ # the packed application would make Bundler look for the build
21
+ # machine's Gemfile, so it must not survive into the executable.
22
+ BUNDLER_SETUP_PATTERN = %r{/bundler/setup\z}
23
+
24
+ # rubyopt: the RUBYOPT string with all translated/removed entries
25
+ # stripped; safe to bake into the executable as-is.
26
+ # load_paths: packed locations (relative to the extraction root) for
27
+ # translated -I entries, in the order given.
28
+ # requires: packed locations (relative to the extraction root) for
29
+ # translated -r entries, in the order given.
30
+ # dropped: -I/-r entries with absolute paths that are not part of
31
+ # the package; they cannot work at runtime and were removed.
32
+ Result = Struct.new(:rubyopt, :load_paths, :requires, :dropped) do
33
+ def translated?
34
+ !load_paths.empty? || !requires.empty?
35
+ end
36
+ end
37
+
38
+ def initialize(rubyopt)
39
+ @rubyopt = rubyopt.to_s
40
+ end
41
+
42
+ # Translates absolute -I/-r paths. For each absolute path the block is
43
+ # called with the path (String) and must return the corresponding
44
+ # location inside the packed application (relative to the extraction
45
+ # root), or nil when the path is not part of the package.
46
+ def translate
47
+ remaining, load_paths, requires, dropped = [], [], [], []
48
+
49
+ words = @rubyopt.split
50
+ until words.empty?
51
+ word = words.shift
52
+ case word
53
+ when /\A-I(.*)\z/m
54
+ arg = $1.empty? ? words.shift : $1
55
+ next if arg.nil?
56
+
57
+ kept = []
58
+ arg.split(File::PATH_SEPARATOR).each do |dir|
59
+ next if dir.empty?
60
+
61
+ if File.absolute_path?(dir)
62
+ if (packed = yield(dir))
63
+ load_paths << packed
64
+ else
65
+ dropped << "-I#{dir}"
66
+ end
67
+ else
68
+ kept << dir
69
+ end
70
+ end
71
+ remaining << "-I#{kept.join(File::PATH_SEPARATOR)}" unless kept.empty?
72
+ when /\A-r(.*)\z/m
73
+ feature = $1.empty? ? words.shift : $1
74
+ next if feature.nil?
75
+
76
+ if feature.match?(BUNDLER_SETUP_PATTERN)
77
+ # Silently stripped, see BUNDLER_SETUP_PATTERN above.
78
+ elsif File.absolute_path?(feature)
79
+ if (packed = yield(feature))
80
+ requires << packed
81
+ else
82
+ dropped << "-r#{feature}"
83
+ end
84
+ else
85
+ remaining << "-r#{feature}"
86
+ end
87
+ else
88
+ remaining << word
89
+ end
90
+ end
91
+
92
+ Result.new(remaining.join(" "), load_paths, requires, dropped)
93
+ end
94
+ end
95
+ end
data/lib/ocran/runner.rb CHANGED
@@ -44,10 +44,141 @@ module Ocran
44
44
  end
45
45
 
46
46
  exit unless @option.run_script?
47
+ if @option.gemfile
48
+ apply_gemfile_to_bundler_env
49
+ else
50
+ warn_about_foreign_bundle
51
+ end
52
+ expose_bundle_install_path
47
53
  say "Loading script to check dependencies"
48
54
  $PROGRAM_NAME = @option.script.to_s
49
55
  end
50
56
 
57
+ # --gemfile names the Gemfile the application runs under, so it has to
58
+ # govern the dependency run as well, not just the later Gemfile scan.
59
+ # The script is loaded in this very process, and a BUNDLE_GEMFILE
60
+ # inherited from the environment would otherwise win: invoke OCRAN from
61
+ # inside another project's `bundle exec` and the script's
62
+ # `require "bundler/setup"` sets up that project's bundle instead of the
63
+ # application's. Gems only the application's Gemfile provides are then
64
+ # missing, and local development gems declared with `path:` or `gemspec`
65
+ # always are, since they exist nowhere else. The dependency run dies with
66
+ # a LoadError before anything can be packed (github issue #34).
67
+ def apply_gemfile_to_bundler_env
68
+ gemfile = @option.gemfile.to_s
69
+ active = active_bundler_gemfile
70
+
71
+ ENV["BUNDLE_GEMFILE"] = gemfile
72
+ return if active.nil? || same_file?(active, gemfile)
73
+
74
+ # Past this point the ambient bundle is a different one, so anything
75
+ # else describing it has to go: BUNDLE_LOCKFILE names the lockfile of
76
+ # the bundle being displaced, and Bundler would sooner believe it than
77
+ # the Gemfile we just pointed it at.
78
+ ENV.delete("BUNDLE_LOCKFILE")
79
+ return unless defined?(Bundler) && Bundler.respond_to?(:reset!)
80
+
81
+ # `bundle exec` puts -rbundler/setup in RUBYOPT as well, so the wrong
82
+ # bundle may already be set up by the time this runs. The variable
83
+ # alone is then too late: bundler/setup sits in $LOADED_FEATURES, and
84
+ # the script's own `require "bundler/setup"` would be a no-op. Drop
85
+ # Bundler's memoized state and let it be set up afresh, against the
86
+ # Gemfile we were given.
87
+ verbose "Rebinding Bundler from #{active} to #{gemfile}"
88
+ Bundler.reset!
89
+ $LOADED_FEATURES.delete_if { |feature| feature.match?(RuntimeEnvironment::BUNDLER_SETUP_FEATURE) }
90
+ end
91
+
92
+ # Without --gemfile the dependency run inherits whatever bundle the
93
+ # environment carries, and when that is some other project's bundle the
94
+ # script's own gems are simply absent. The failure that follows is a
95
+ # LoadError from deep inside the script, which says nothing about the
96
+ # bundle being the wrong one, so say it here while the reason is still
97
+ # visible.
98
+ def warn_about_foreign_bundle
99
+ # Only when a bundle really is set up. BUNDLE_GEMFILE alone is
100
+ # inherited by any child process and governs nothing on its own.
101
+ return unless @pre_env.bundler_setup_loaded?
102
+
103
+ active = active_bundler_gemfile
104
+ return if active.nil? || active.empty?
105
+
106
+ app_gemfile = @option.application_gemfile
107
+ return if app_gemfile && same_file?(active, app_gemfile)
108
+
109
+ warning "Running under Bundler with #{active}, which is not the Gemfile " \
110
+ "#{@option.script} runs under" +
111
+ (app_gemfile ? "; pass --gemfile #{app_gemfile} to package against that bundle" : "")
112
+ end
113
+
114
+ # `bundle install` puts the gems where Bundler is configured to put them,
115
+ # and a configured path - `bundle config set path vendor/bundle`, which is
116
+ # what ruby/setup-ruby's `bundler-cache: true` does - is a place only
117
+ # Bundler looks. The dependency run loads the script as plain Ruby, so
118
+ # its first `require` of a gem from such a bundle raised LoadError
119
+ # although the bundle was installed (github issue #61).
120
+ #
121
+ # Add the bundle's install directory to the gem path. Its gems become
122
+ # visible exactly as if they had been installed into Ruby's own gem
123
+ # directory, and nothing more: setting the bundle up would also hide
124
+ # every gem the Gemfile does not list, breaking scripts that work today.
125
+ # A bundle installed into the system gems, the ordinary case outside CI,
126
+ # changes nothing, and under `bundle exec` Bundler has already put its
127
+ # gems on the gem path.
128
+ def expose_bundle_install_path
129
+ return if @pre_env.bundler_setup_loaded?
130
+
131
+ gemfile = @option.application_gemfile or return
132
+ path = bundle_install_path(gemfile) or return
133
+ return if Gem.path.any? { |gem_path| same_file?(gem_path, path) }
134
+
135
+ verbose "Adding #{path}, where the bundle of #{gemfile} is installed, to the gem path"
136
+ Gem.paths = { "GEM_HOME" => Gem.dir, "GEM_PATH" => (Gem.path + [path]).join(File::PATH_SEPARATOR) }
137
+ end
138
+
139
+ # Ruby run in a child process to print where Bundler installs the bundle
140
+ # of BUNDLE_GEMFILE, or nothing when that is the system gems.
141
+ BUNDLE_PATH_QUERY = <<~RUBY
142
+ require "bundler"
143
+ print Bundler.bundle_path unless Bundler.configured_bundle_path.use_system_gems?
144
+ RUBY
145
+ private_constant :BUNDLE_PATH_QUERY
146
+
147
+ # The directory the bundle of the given Gemfile is installed into when
148
+ # that is not the system gems, or nil. Bundler decides it from its
149
+ # settings - .bundle/config beside the Gemfile, the user's configuration
150
+ # and BUNDLE_* variables - without evaluating the Gemfile.
151
+ #
152
+ # Asked in a child process: loading Bundler here, before the dependency
153
+ # run, would put Bundler's files among the script's loaded features and
154
+ # pack them into every application with a Gemfile.
155
+ def bundle_install_path(gemfile)
156
+ env = { "BUNDLE_GEMFILE" => gemfile.to_s, "RUBYOPT" => nil, "BUNDLER_SETUP" => nil }
157
+ path = IO.popen(env, [RbConfig.ruby, "-e", BUNDLE_PATH_QUERY], err: File::NULL, &:read)
158
+ return nil unless $?.success? && !path.empty? && File.directory?(path)
159
+
160
+ path
161
+ rescue SystemCallError => e
162
+ verbose "Could not ask Bundler where the bundle of #{gemfile} is installed: #{e.message}"
163
+ nil
164
+ end
165
+
166
+ # The Gemfile this process was started against, if any.
167
+ def active_bundler_gemfile
168
+ return ENV["BUNDLE_GEMFILE"] unless ENV["BUNDLE_GEMFILE"].to_s.empty?
169
+ return nil unless defined?(Bundler) && Bundler.respond_to?(:default_gemfile)
170
+
171
+ Bundler.default_gemfile.to_s
172
+ rescue StandardError
173
+ # Bundler raises when there is no Gemfile anywhere above the working
174
+ # directory, which just means there is no ambient bundle to displace.
175
+ nil
176
+ end
177
+
178
+ def same_file?(a, b)
179
+ File.identical?(a, b) || File.expand_path(a) == File.expand_path(b)
180
+ end
181
+
51
182
  # Force loading autoloaded constants. Searches through all modules
52
183
  # (and hence classes), and checks their constants for autoloaded
53
184
  # ones, then attempts to load them.
@@ -107,6 +238,8 @@ module Ocran
107
238
  direction.build_output_dir(@option.output_dir)
108
239
  elsif @option.output_zip
109
240
  direction.build_zip(@option.output_zip)
241
+ elsif @option.cosmo_zip?
242
+ direction.build_cosmo_zip_exe
110
243
  else
111
244
  direction.build_stab_exe
112
245
  end
@@ -10,13 +10,39 @@ module Ocran
10
10
  alias save new
11
11
  end
12
12
 
13
- attr_reader :env, :load_path, :loaded_features, :pwd
13
+ # Matches the bundler/setup feature by which `bundle exec` sets a
14
+ # process up, whatever prefix the bundler gem happens to be installed
15
+ # under.
16
+ BUNDLER_SETUP_FEATURE = %r{[\\/]bundler[\\/]setup\.rb\z}
17
+
18
+ attr_reader :env, :load_path, :loaded_features, :pwd, :loaded_specs
14
19
 
15
20
  def initialize
16
21
  @env = ENV.to_hash.freeze
17
22
  @load_path = $LOAD_PATH.dup.freeze
18
23
  @loaded_features = $LOADED_FEATURES.dup.freeze
19
24
  @pwd = Dir.pwd.freeze
25
+ # The specs of the gems RubyGems had activated at this point. Taken
26
+ # with the loaded features for the same reason: OCRAN goes on to
27
+ # activate gems of its own while it builds (fiddle for DLL detection,
28
+ # tempfile for the builder, ...), and those are not the application's.
29
+ # Under `bundle exec` the snapshot taken before the dependency run is
30
+ # the entire bundle, activated before OCRAN's first line runs, which is
31
+ # what tells the build environment's gems apart from the application's.
32
+ @loaded_specs = (defined?(Gem) ? Gem.loaded_specs.values : []).freeze
33
+ end
34
+
35
+ # The names of the gems in #loaded_specs.
36
+ def activated_gems
37
+ @loaded_specs.map(&:name)
38
+ end
39
+
40
+ # Whether Bundler had already set this process up when the snapshot was
41
+ # taken. `bundle exec` arranges that through RUBYOPT (and, with newer
42
+ # RubyGems, through the BUNDLER_SETUP variable), so bundler/setup is in
43
+ # $LOADED_FEATURES before the command it runs gets to say anything.
44
+ def bundler_setup_loaded?
45
+ @loaded_features.any? { |feature| feature.to_s.match?(BUNDLER_SETUP_FEATURE) }
20
46
  end
21
47
 
22
48
  # Expands the given path using the working directory stored in this
@@ -21,6 +21,7 @@ module Ocran
21
21
  CHDIR_BEFORE_SCRIPT = 0x08
22
22
  DATA_COMPRESSED = 0x10
23
23
  RUN_IN_EXE_DIR = 0x20
24
+ CHDIR_TO_EXE_DIR = 0x40
24
25
 
25
26
  WINDOWS = Gem.win_platform?
26
27
 
@@ -77,6 +78,12 @@ module Ocran
77
78
  # When set to true, the working directory is changed to the application's
78
79
  # deployment location at runtime.
79
80
  #
81
+ # chdir_to_exe_dir:
82
+ # When set to true, the working directory is changed to the directory
83
+ # containing the (stub) executable before the script starts. This makes
84
+ # relative file access resolve next to the executable regardless of how
85
+ # it was invoked. Mutually exclusive with chdir_before.
86
+ #
80
87
  # debug_mode:
81
88
  # When the debug_mode option is set to true, the stub will output debug information
82
89
  # when the exe file is executed. Debug mode can also be enabled within the directive
@@ -103,9 +110,16 @@ module Ocran
103
110
  # are installed next to the stub (pre-1.4/OCRA behavior). The directory
104
111
  # is never deleted on exit.
105
112
  #
106
- def initialize(path, chdir_before: nil, debug_extract: nil, debug_mode: nil,
113
+ # stub_path:
114
+ # Path to a stub binary to package with instead of the pre-built
115
+ # stub shipped with the gem (e.g. an APE stub freshly compiled with
116
+ # cosmocc, see --cosmo). When set, it takes precedence over both
117
+ # STUB_PATH and STUBW_PATH.
118
+ #
119
+ def initialize(path, chdir_before: nil, chdir_to_exe_dir: nil,
120
+ debug_extract: nil, debug_mode: nil,
107
121
  enable_compression: nil, gui_mode: nil, icon_path: nil,
108
- run_in_exe_dir: nil)
122
+ run_in_exe_dir: nil, stub_path: nil)
109
123
  @dirs = FilePathSet.new
110
124
  @files = FilePathSet.new
111
125
  @data_size = 0
@@ -117,7 +131,9 @@ module Ocran
117
131
  output_dir = File.dirname(path)
118
132
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
119
133
  stub_tmp = File.join(output_dir, ".ocran_stub_#{$$}_#{Time.now.to_i}")
120
- stub_src = if gui_mode && WINDOWS
134
+ stub_src = if stub_path
135
+ stub_path
136
+ elsif gui_mode && WINDOWS
121
137
  STUBW_PATH
122
138
  else
123
139
  STUB_PATH
@@ -138,7 +154,7 @@ module Ocran
138
154
  @of = of
139
155
  @opcode_offset = @of.size
140
156
 
141
- write_header(debug_mode, debug_extract, chdir_before, enable_compression, run_in_exe_dir)
157
+ write_header(debug_mode, debug_extract, chdir_before, enable_compression, run_in_exe_dir, chdir_to_exe_dir)
142
158
 
143
159
  b = proc {
144
160
  yield(self)
@@ -222,7 +238,7 @@ module Ocran
222
238
  end
223
239
  private :compress
224
240
 
225
- def write_header(debug_mode, debug_extract, chdir_before, compressed, run_in_exe_dir = nil)
241
+ def write_header(debug_mode, debug_extract, chdir_before, compressed, run_in_exe_dir = nil, chdir_to_exe_dir = nil)
226
242
  next_to_exe, delete_after = debug_extract, !debug_extract
227
243
  if run_in_exe_dir
228
244
  # Wrapper mode: run in place next to the executable — never extract,
@@ -235,7 +251,8 @@ module Ocran
235
251
  (delete_after ? AUTO_CLEAN_INST_DIR : 0) |
236
252
  (chdir_before ? CHDIR_BEFORE_SCRIPT : 0) |
237
253
  (compressed ? DATA_COMPRESSED : 0) |
238
- (run_in_exe_dir ? RUN_IN_EXE_DIR : 0)
254
+ (run_in_exe_dir ? RUN_IN_EXE_DIR : 0) |
255
+ (chdir_to_exe_dir ? CHDIR_TO_EXE_DIR : 0)
239
256
  ].pack("C")
240
257
  end
241
258
  private :write_header
data/lib/ocran/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ocran
4
- VERSION = "1.4.4"
4
+ VERSION = "1.4.6"
5
5
  end