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.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +37 -0
- data/README.md +539 -31
- data/lib/ocran/build_facade.rb +2 -0
- data/lib/ocran/cosmo_toolchain.rb +383 -0
- data/lib/ocran/dir_builder.rb +3 -0
- data/lib/ocran/direction.rb +819 -64
- data/lib/ocran/gem_spec_queryable.rb +24 -3
- data/lib/ocran/inno_setup_script_builder.rb +15 -1
- data/lib/ocran/launcher_event_recorder.rb +29 -0
- data/lib/ocran/option.rb +172 -3
- data/lib/ocran/rubyopt_processor.rb +95 -0
- data/lib/ocran/runner.rb +84 -2
- data/lib/ocran/runtime_environment.rb +19 -1
- data/lib/ocran/stub_builder.rb +38 -6
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/zip_payload_builder.rb +282 -0
- data/lib/ocran/zip_writer.rb +287 -0
- data/src/Makefile +9 -0
- data/src/inst_dir.c +44 -2
- data/src/inst_dir.h +14 -0
- data/src/script_info.c +15 -2
- data/src/script_info.h +15 -1
- data/src/stub.c +70 -9
- data/src/system_utils.c +26 -0
- data/src/system_utils.h +14 -0
- data/src/system_utils_posix.c +46 -0
- data/src/unpack.c +8 -0
- data/src/unpack.h +19 -0
- metadata +15 -24
|
@@ -131,8 +131,11 @@ module Ocran
|
|
|
131
131
|
|
|
132
132
|
def gem_root = Pathname(gem_dir)
|
|
133
133
|
|
|
134
|
-
# Find the selected files
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|