ocran 1.4.3 → 1.4.5

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.
@@ -131,8 +131,11 @@ module Ocran
131
131
 
132
132
  def gem_root = Pathname(gem_dir)
133
133
 
134
- # Find the selected files
135
- def gem_root_files = gem_root.find.select(&:file?)
134
+ # Find the selected files. Default gems (e.g. fiddle, singleton on
135
+ # Homebrew or distro-packaged Ruby) may have a gemspec without a
136
+ # materialized gem directory - their files live in Ruby's stdlib and
137
+ # are packed via the load path instead.
138
+ def gem_root_files = gem_root.directory? ? gem_root.find.select(&:file?) : []
136
139
 
137
140
  def script_files
138
141
  gem_root_files.select { |path| path.extname =~ GEM_SCRIPT_RE }
@@ -154,7 +157,25 @@ module Ocran
154
157
  when :spec
155
158
  files.map { |file| Pathname(file) }
156
159
  when :loaded
157
- features_from_gems.select { |feature| feature.subpath?(gem_dir) }
160
+ # Some distros expose gem files under additional paths via symlinks
161
+ # (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the psych gem
162
+ # directory) and $LOADED_FEATURES records the symlink path. Resolve
163
+ # each feature to its realpath so such files count as gem files and
164
+ # are packed inside the gem directory. Packing them at the symlink
165
+ # location instead would break require_relative, which resolves
166
+ # against the realpath on the build host but not in the packed app.
167
+ features_from_gems.filter_map do |feature|
168
+ if feature.subpath?(gem_dir)
169
+ feature
170
+ else
171
+ real = begin
172
+ feature.realpath
173
+ rescue SystemCallError
174
+ next
175
+ end
176
+ real if real.subpath?(gem_dir)
177
+ end
178
+ end
158
179
  when :files
159
180
  resource_files
160
181
  when :extras
@@ -14,7 +14,14 @@ module Ocran
14
14
 
15
15
  class << self
16
16
  def compile(iss_filename, quiet: false)
17
- unless Gem.win_platform? || system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
17
+ # "where" and ">NUL" only exist on Windows; use "command -v" on POSIX
18
+ # (e.g. when testing the pipeline with a fake ISCC on Linux/macOS).
19
+ iscc_found = if Gem.win_platform?
20
+ true # ISCC is invoked directly; failure is reported below
21
+ else
22
+ system("command -v #{quote_and_escape(ISCC_CMD)} > /dev/null 2>&1")
23
+ end
24
+ unless iscc_found
18
25
  raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
19
26
  end
20
27
 
@@ -79,6 +86,13 @@ module Ocran
79
86
  @dirs.add?("/", target)
80
87
  end
81
88
 
89
+ # Symbolic links cannot be expressed in an Inno Setup script. They only
90
+ # occur when building from POSIX hosts (e.g. libruby.so links); Windows
91
+ # installations do not need them, so they are skipped.
92
+ def symlink(_target, _link_name)
93
+ nil
94
+ end
95
+
82
96
  def cp(source, target)
83
97
  unless File.exist?(source)
84
98
  raise "The file does not exist (#{source})"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ # Records launcher-directed build events (environment exports and the
5
+ # application exec command) while forwarding them to another launcher
6
+ # builder. The recorded events can later be replayed into a further
7
+ # builder — e.g. to produce a wrapper stub executable alongside the
8
+ # launcher batch file in Inno Setup builds.
9
+ class LauncherEventRecorder
10
+ def initialize(launcher)
11
+ @launcher = launcher
12
+ @events = []
13
+ end
14
+
15
+ def export(name, value)
16
+ @events << [:export, name, value]
17
+ @launcher.export(name, value)
18
+ end
19
+
20
+ def exec(image, script, *argv)
21
+ @events << [:exec, image, script, *argv]
22
+ @launcher.exec(image, script, *argv)
23
+ end
24
+
25
+ def replay(builder)
26
+ @events.each { |event, *args| builder.public_send(event, *args) }
27
+ end
28
+ end
29
+ end
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,
@@ -37,6 +41,7 @@ module Ocran
37
41
  :source_files => [],
38
42
  :verbose? => false,
39
43
  :warning? => true,
44
+ :wrapper_exe? => true,
40
45
  }
41
46
  end
42
47
 
@@ -90,6 +95,8 @@ Output options:
90
95
  --output <file> Name the exe to generate. Defaults to ./<scriptname>.exe.
91
96
  --output-dir <dir> Output all files to a directory with a launch script instead of an exe.
92
97
  --output-zip <file> Output a zip archive containing all files and a launch script.
98
+ --no-wrapper-exe Do not add the wrapper executable to installer, directory or
99
+ zip output (a launch script is always included there).
93
100
  --macosx-bundle Build a macOS .app bundle. Use --output to name it (default: <scriptname>.app).
94
101
  --bundle-id <id> Bundle identifier for the macOS app bundle (default: com.example.<appname>).
95
102
  --no-lzma Disable LZMA compression of the executable.
@@ -100,13 +107,62 @@ Executable options:
100
107
  --windows Force Windows application (rubyw.exe)
101
108
  --console Force console application (ruby.exe)
102
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.
103
112
  --icon <ico> Replace icon with a custom one.
104
- --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.
105
116
  --debug Executable will be verbose.
106
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)
107
147
  EOF
108
148
  end
109
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
+
110
166
  def parse(argv)
111
167
  while (arg = argv.shift)
112
168
  case arg
@@ -125,6 +181,8 @@ EOF
125
181
  when "--output-zip"
126
182
  path = argv.shift
127
183
  @options[:output_zip] = Pathname.new(path).expand_path if path
184
+ when "--no-wrapper-exe"
185
+ @options[:wrapper_exe?] = false
128
186
  when "--macosx-bundle"
129
187
  @options[:macosx_bundle?] = true
130
188
  when "--bundle-id"
@@ -144,12 +202,22 @@ EOF
144
202
  @options[:load_autoload?] = false
145
203
  when "--chdir-first"
146
204
  @options[:chdir_before?] = true
205
+ when "--chdir-exe-dir"
206
+ @options[:chdir_exe_dir?] = true
147
207
  when "--icon"
148
208
  path = argv.shift
149
209
  raise "Icon file #{path} not found" unless path && File.exist?(path)
150
210
  @options[:icon_filename] = Pathname.new(path).expand_path
151
211
  when "--rubyopt"
152
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)
153
221
  when "--gemfile"
154
222
  path = argv.shift
155
223
  raise "Gemfile #{path} not found" unless path && File.exist?(path)
@@ -214,8 +282,15 @@ EOF
214
282
  executable = script
215
283
  # If debug mode is enabled, append "-debug" to the filename
216
284
  executable = executable.append_to_filename("-debug") if enable_debug_mode?
217
- # Build output files are created in the current directory
218
- ext = Gem.win_platform? ? ".exe" : ""
285
+ # Build output files are created in the current directory.
286
+ # APE binaries built with cosmocc conventionally use .com.
287
+ ext = if cosmo?
288
+ ".com"
289
+ elsif Gem.win_platform?
290
+ ".exe"
291
+ else
292
+ ""
293
+ end
219
294
  executable.basename.sub_ext(ext).expand_path
220
295
  end
221
296
 
@@ -224,6 +299,10 @@ EOF
224
299
  @options[:macosx_bundle] = Pathname(bundle_base).sub_ext(".app").expand_path
225
300
  end
226
301
 
302
+ if chdir_before? && chdir_exe_dir?
303
+ raise "--chdir-first and --chdir-exe-dir cannot be used together"
304
+ end
305
+
227
306
  @options[:use_inno_setup?] = !!inno_setup_script
228
307
 
229
308
  @options[:verbose?] &&= !quiet?
@@ -248,6 +327,41 @@ EOF
248
327
  raise "--output-dir and --output-zip cannot be used together"
249
328
  end
250
329
 
330
+ if cosmo?
331
+ opt = cosmo_cc ? "--cosmo" : "--cosmo-ruby"
332
+
333
+ if Gem.win_platform?
334
+ raise "#{opt} is not supported when building on Windows (build the APE package on a Linux/macOS host)"
335
+ end
336
+
337
+ if force_windows?
338
+ raise "--windows cannot be used with #{opt}: cosmocc has no GUI (stubw) equivalent; APE stubs are console-only"
339
+ end
340
+
341
+ load_cosmo_toolchain
342
+
343
+ # A cosmopolitan Ruby that runs an embedded /zip/main.rb can carry
344
+ # the application inside its own ZIP store, which needs no compiler
345
+ # and no launcher stub at all - so that is what --cosmo-ruby does
346
+ # on its own. Naming a toolchain with --cosmo is the way to ask for
347
+ # the launcher stub explicitly; the other output formats
348
+ # (--output-dir, --output-zip, --innosetup, --macosx-bundle) are
349
+ # directory layouts rather than a single binary, so they keep using
350
+ # it too.
351
+ @options[:cosmo_zip?] =
352
+ !!cosmo_ruby && !cosmo_cc && !single_binary_output_conflict &&
353
+ CosmoToolchain.zip_main_support?(cosmo_ruby)
354
+
355
+ unless cosmo_zip?
356
+ # The APE stub is compiled with cosmocc. --cosmo names that
357
+ # toolchain explicitly; otherwise it is discovered on the build
358
+ # host (COSMOCC, PATH, conventional install locations), so one
359
+ # option is still enough. Resolved here, before the dependency
360
+ # run, so a missing toolchain fails fast.
361
+ @options[:cosmo_cc] = CosmoToolchain.require_cc(cosmo_cc)
362
+ end
363
+ end
364
+
251
365
  if macosx_bundle && (output_dir || output_zip || inno_setup_script)
252
366
  raise "--macosx-bundle cannot be combined with --output-dir, --output-zip, or --innosetup"
253
367
  end
@@ -267,6 +381,29 @@ EOF
267
381
 
268
382
  def chdir_before? = @options[__method__]
269
383
 
384
+ def chdir_exe_dir? = @options[__method__]
385
+ def cosmo_cc = @options[__method__]
386
+
387
+ def cosmo_ruby = @options[__method__]
388
+
389
+ # True when the application is packaged by injecting it into the ZIP
390
+ # store of the cosmopolitan Ruby itself: no cosmocc, no launcher stub,
391
+ # and no extraction to a temporary directory at run time.
392
+ def cosmo_zip? = @options[__method__]
393
+
394
+ # Output formats that are not a single self-contained binary, and
395
+ # therefore cannot be produced by injecting into the interpreter.
396
+ def single_binary_output_conflict
397
+ output_dir || output_zip || inno_setup_script || @options[:macosx_bundle?]
398
+ end
399
+ private :single_binary_output_conflict
400
+
401
+ # True when the build produces an APE (Actually Portable Executable),
402
+ # i.e. an APE launcher stub is used: an explicit toolchain (--cosmo), a
403
+ # cosmopolitan Ruby payload (--cosmo-ruby, whose toolchain is inferred),
404
+ # or both.
405
+ def cosmo? = !!(cosmo_cc || cosmo_ruby)
406
+
270
407
  def enable_compression? = @options[__method__]
271
408
 
272
409
  def enable_debug_extract? = @options[__method__]
@@ -301,12 +438,44 @@ EOF
301
438
 
302
439
  def quiet? = @options[__method__]
303
440
 
441
+ def wrapper_exe? = @options[__method__]
442
+
304
443
  def rubyopt = @options[__method__]
305
444
 
306
445
  def run_script? = @options[__method__]
307
446
 
308
447
  def script = @options[__method__]
309
448
 
449
+ # The names Bundler accepts for a Gemfile, most common first.
450
+ GEMFILE_NAMES = %w[Gemfile gems.rb].freeze
451
+ private_constant :GEMFILE_NAMES
452
+
453
+ # The Gemfile the application runs under: the one named by --gemfile,
454
+ # or else the nearest Gemfile at or above the script's directory, which
455
+ # is where Bundler itself would look. Returns nil when the application
456
+ # has no Gemfile at all.
457
+ #
458
+ # This is how the application's own bundle is told apart from whatever
459
+ # bundle OCRAN happens to have been started under. Running
460
+ # `bundle exec ocran app.rb` from the application's own directory is the
461
+ # ordinary case, and there the two are the same file; packaging some
462
+ # other project from inside this one's bundle is not.
463
+ def application_gemfile
464
+ return gemfile if gemfile
465
+
466
+ dir = script.expand_path.dirname
467
+ loop do
468
+ GEMFILE_NAMES.each do |name|
469
+ candidate = dir + name
470
+ return candidate if candidate.file?
471
+ end
472
+ parent = dir.parent
473
+ return nil if parent == dir
474
+
475
+ dir = parent
476
+ end
477
+ end
478
+
310
479
  def source_files = @options[__method__]
311
480
 
312
481
  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,88 @@ 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
47
52
  say "Loading script to check dependencies"
48
53
  $PROGRAM_NAME = @option.script.to_s
49
54
  end
50
55
 
56
+ # --gemfile names the Gemfile the application runs under, so it has to
57
+ # govern the dependency run as well, not just the later Gemfile scan.
58
+ # The script is loaded in this very process, and a BUNDLE_GEMFILE
59
+ # inherited from the environment would otherwise win: invoke OCRAN from
60
+ # inside another project's `bundle exec` and the script's
61
+ # `require "bundler/setup"` sets up that project's bundle instead of the
62
+ # application's. Gems only the application's Gemfile provides are then
63
+ # missing, and local development gems declared with `path:` or `gemspec`
64
+ # always are, since they exist nowhere else. The dependency run dies with
65
+ # a LoadError before anything can be packed (github issue #34).
66
+ def apply_gemfile_to_bundler_env
67
+ gemfile = @option.gemfile.to_s
68
+ active = active_bundler_gemfile
69
+
70
+ ENV["BUNDLE_GEMFILE"] = gemfile
71
+ return if active.nil? || same_file?(active, gemfile)
72
+
73
+ # Past this point the ambient bundle is a different one, so anything
74
+ # else describing it has to go: BUNDLE_LOCKFILE names the lockfile of
75
+ # the bundle being displaced, and Bundler would sooner believe it than
76
+ # the Gemfile we just pointed it at.
77
+ ENV.delete("BUNDLE_LOCKFILE")
78
+ return unless defined?(Bundler) && Bundler.respond_to?(:reset!)
79
+
80
+ # `bundle exec` puts -rbundler/setup in RUBYOPT as well, so the wrong
81
+ # bundle may already be set up by the time this runs. The variable
82
+ # alone is then too late: bundler/setup sits in $LOADED_FEATURES, and
83
+ # the script's own `require "bundler/setup"` would be a no-op. Drop
84
+ # Bundler's memoized state and let it be set up afresh, against the
85
+ # Gemfile we were given.
86
+ verbose "Rebinding Bundler from #{active} to #{gemfile}"
87
+ Bundler.reset!
88
+ $LOADED_FEATURES.delete_if { |feature| feature.match?(RuntimeEnvironment::BUNDLER_SETUP_FEATURE) }
89
+ end
90
+
91
+ # Without --gemfile the dependency run inherits whatever bundle the
92
+ # environment carries, and when that is some other project's bundle the
93
+ # script's own gems are simply absent. The failure that follows is a
94
+ # LoadError from deep inside the script, which says nothing about the
95
+ # bundle being the wrong one, so say it here while the reason is still
96
+ # visible.
97
+ def warn_about_foreign_bundle
98
+ # Only when a bundle really is set up. BUNDLE_GEMFILE alone is
99
+ # inherited by any child process and governs nothing on its own.
100
+ return unless @pre_env.bundler_setup_loaded?
101
+
102
+ active = active_bundler_gemfile
103
+ return if active.nil? || active.empty?
104
+
105
+ app_gemfile = @option.application_gemfile
106
+ return if app_gemfile && same_file?(active, app_gemfile)
107
+
108
+ warning "Running under Bundler with #{active}, which is not the Gemfile " \
109
+ "#{@option.script} runs under" +
110
+ (app_gemfile ? "; pass --gemfile #{app_gemfile} to package against that bundle" : "")
111
+ end
112
+
113
+ # The Gemfile this process was started against, if any.
114
+ def active_bundler_gemfile
115
+ return ENV["BUNDLE_GEMFILE"] unless ENV["BUNDLE_GEMFILE"].to_s.empty?
116
+ return nil unless defined?(Bundler) && Bundler.respond_to?(:default_gemfile)
117
+
118
+ Bundler.default_gemfile.to_s
119
+ rescue StandardError
120
+ # Bundler raises when there is no Gemfile anywhere above the working
121
+ # directory, which just means there is no ambient bundle to displace.
122
+ nil
123
+ end
124
+
125
+ def same_file?(a, b)
126
+ File.identical?(a, b) || File.expand_path(a) == File.expand_path(b)
127
+ end
128
+
51
129
  # Force loading autoloaded constants. Searches through all modules
52
130
  # (and hence classes), and checks their constants for autoloaded
53
131
  # ones, then attempts to load them.
@@ -94,10 +172,12 @@ module Ocran
94
172
  direction = Direction.new(@post_env, @pre_env, @option)
95
173
 
96
174
  if @option.use_inno_setup?
97
- if Gem.win_platform?
175
+ # Native on Windows; on POSIX allow it when an ISCC command is
176
+ # available (e.g. via Wine wrapper scripts or in tests).
177
+ if Gem.win_platform? || system("command -v ISCC > /dev/null 2>&1")
98
178
  direction.build_inno_setup_installer
99
179
  else
100
- raise "Inno Setup is only supported on Windows"
180
+ raise "Inno Setup is only supported on Windows (no ISCC command found in PATH)"
101
181
  end
102
182
  elsif @option.macosx_bundle
103
183
  direction.build_macosx_bundle(@option.macosx_bundle)
@@ -105,6 +185,8 @@ module Ocran
105
185
  direction.build_output_dir(@option.output_dir)
106
186
  elsif @option.output_zip
107
187
  direction.build_zip(@option.output_zip)
188
+ elsif @option.cosmo_zip?
189
+ direction.build_cosmo_zip_exe
108
190
  else
109
191
  direction.build_stab_exe
110
192
  end
@@ -10,13 +10,31 @@ 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, :activated_gems
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 gems RubyGems had activated at this point. Under `bundle exec`
26
+ # that is the entire bundle, activated before OCRAN's first line runs,
27
+ # so a snapshot taken before the dependency run is what tells the build
28
+ # environment's gems apart from the application's.
29
+ @activated_gems = (defined?(Gem) ? Gem.loaded_specs.keys : []).freeze
30
+ end
31
+
32
+ # Whether Bundler had already set this process up when the snapshot was
33
+ # taken. `bundle exec` arranges that through RUBYOPT (and, with newer
34
+ # RubyGems, through the BUNDLER_SETUP variable), so bundler/setup is in
35
+ # $LOADED_FEATURES before the command it runs gets to say anything.
36
+ def bundler_setup_loaded?
37
+ @loaded_features.any? { |feature| feature.to_s.match?(BUNDLER_SETUP_FEATURE) }
20
38
  end
21
39
 
22
40
  # Expands the given path using the working directory stored in this