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.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +24 -0
- data/README.md +437 -16
- data/lib/ocran/cosmo_toolchain.rb +383 -0
- data/lib/ocran/direction.rb +632 -59
- data/lib/ocran/option.rb +174 -4
- data/lib/ocran/rubyopt_processor.rb +95 -0
- data/lib/ocran/runner.rb +133 -0
- data/lib/ocran/runtime_environment.rb +27 -1
- data/lib/ocran/stub_builder.rb +23 -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/script_info.c +15 -2
- data/src/script_info.h +15 -1
- data/src/stub.c +51 -1
- data/src/system_utils_posix.c +40 -0
- data/src/unpack.c +4 -0
- data/src/unpack.h +10 -0
- metadata +6 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/ocran/stub_builder.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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