ocran 1.4.5 → 1.5.0
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 +74 -0
- data/README.md +296 -9
- data/ext/extconf.rb +5 -1
- data/lib/ocran/aot_toolchain.rb +154 -0
- data/lib/ocran/command_output.rb +8 -1
- data/lib/ocran/compatibility_scan.rb +414 -0
- data/lib/ocran/cosmo_toolchain.rb +29 -7
- data/lib/ocran/dir_builder.rb +88 -38
- data/lib/ocran/direction.rb +232 -33
- data/lib/ocran/ed_resource.rb +113 -0
- data/lib/ocran/ed_resource_builder.rb +199 -0
- data/lib/ocran/file_path_set.rb +9 -5
- data/lib/ocran/gem_spec_queryable.rb +7 -3
- data/lib/ocran/host_config_helper.rb +11 -1
- data/lib/ocran/inno_setup_script_builder.rb +24 -14
- data/lib/ocran/launcher_batch_builder.rb +8 -11
- data/lib/ocran/npm_package.rb +106 -0
- data/lib/ocran/option.rb +433 -35
- data/lib/ocran/pe_imports.rb +96 -0
- data/lib/ocran/pe_resources.rb +226 -0
- data/lib/ocran/refine_pathname.rb +29 -32
- data/lib/ocran/roundhouse_builder.rb +316 -0
- data/lib/ocran/runner.rb +96 -11
- data/lib/ocran/runtime_environment.rb +14 -6
- data/lib/ocran/runtime_sdk.rb +252 -0
- data/lib/ocran/spinel_builder.rb +183 -0
- data/lib/ocran/spinel_compatibility.rb +149 -0
- data/lib/ocran/static_runtime/boot.rb +443 -0
- data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
- data/lib/ocran/static_runtime_builder.rb +814 -0
- data/lib/ocran/stub_builder.rb +70 -25
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/wasm_builder.rb +570 -0
- data/lib/ocran/wasm_compatibility.rb +198 -0
- data/lib/ocran/windows_command_escaping.rb +21 -0
- data/lib/ocran/zip_payload_builder.rb +18 -9
- data/lib/ocran/zip_writer.rb +48 -9
- data/src/Makefile +18 -2
- data/src/Makefile.mswin +65 -0
- data/src/inst_dir.c +34 -6
- data/src/path_utils.c +144 -0
- data/src/script_info.c +5 -0
- data/src/stub.c +77 -19
- data/src/stub.rc +1 -1
- data/src/system_utils.c +276 -155
- data/src/system_utils.h +46 -3
- data/src/system_utils_posix.c +243 -148
- data/src/unpack.c +9 -0
- metadata +23 -5
data/lib/ocran/dir_builder.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
require "pathname"
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "build_constants"
|
|
5
|
+
require_relative "windows_command_escaping"
|
|
5
6
|
|
|
6
7
|
module Ocran
|
|
7
8
|
# Builder that outputs all files to a plain directory instead of a self-extracting
|
|
8
9
|
# executable. A launch script (`.sh` on POSIX, `.bat` on Windows) is written at
|
|
9
10
|
# the root of the directory so the packaged app can be started directly.
|
|
10
11
|
class DirBuilder
|
|
11
|
-
include BuildConstants
|
|
12
|
+
include BuildConstants, WindowsCommandEscaping
|
|
12
13
|
|
|
13
14
|
WINDOWS = Gem.win_platform?
|
|
14
15
|
|
|
@@ -17,8 +18,20 @@ module Ocran
|
|
|
17
18
|
# by Direction to build the optional wrapper executable.
|
|
18
19
|
attr_reader :env, :exec_args
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
# +script_name+ names the launch script (without extension); it defaults
|
|
22
|
+
# to the name of the script that is executed. Direction passes the
|
|
23
|
+
# user's script, because what is executed can be a generated launcher
|
|
24
|
+
# (see Direction#generate_rubyopt_launcher).
|
|
25
|
+
#
|
|
26
|
+
# +chdir_before+ and +chdir_exe_dir+ mirror the stub flags of the same
|
|
27
|
+
# names (--chdir-first, --chdir-exe-dir): the launch script changes into
|
|
28
|
+
# the directory of the packed script, or into its own directory, which
|
|
29
|
+
# is where the output directory keeps the application.
|
|
30
|
+
def initialize(path, script_name: nil, chdir_before: false, chdir_exe_dir: false)
|
|
21
31
|
@path = Pathname(path)
|
|
32
|
+
@script_name = script_name
|
|
33
|
+
@chdir_before = chdir_before
|
|
34
|
+
@chdir_exe_dir = chdir_exe_dir
|
|
22
35
|
@path.mkpath
|
|
23
36
|
@env = {}
|
|
24
37
|
@exec_args = nil
|
|
@@ -55,17 +68,33 @@ module Ocran
|
|
|
55
68
|
@exec_args = [image.to_s, script.to_s, argv.map(&:to_s)]
|
|
56
69
|
end
|
|
57
70
|
|
|
58
|
-
#
|
|
71
|
+
# PowerShell command for create_zip on Windows. The paths come in through
|
|
72
|
+
# the environment rather than being spliced into the command, so no
|
|
73
|
+
# character in them (a quote in a user name, brackets) can change its
|
|
74
|
+
# meaning; -LiteralPath keeps wildcards in the source path literal.
|
|
75
|
+
POWERSHELL_ZIP_COMMAND =
|
|
76
|
+
"$ErrorActionPreference = 'Stop'; " \
|
|
77
|
+
"Get-ChildItem -LiteralPath $env:OCRAN_ZIP_SOURCE -Force | " \
|
|
78
|
+
"Compress-Archive -DestinationPath $env:OCRAN_ZIP_DESTINATION"
|
|
79
|
+
|
|
80
|
+
# Create a zip archive from a source directory, replacing any archive
|
|
81
|
+
# already at +zip_path+.
|
|
59
82
|
# Uses the `zip` command on POSIX and PowerShell on Windows.
|
|
60
83
|
def self.create_zip(zip_path, source_dir)
|
|
61
84
|
zip_path = File.expand_path(zip_path.to_s)
|
|
85
|
+
# Both tools add to an existing archive rather than replace it (zip
|
|
86
|
+
# keeps entries the new tree no longer has; Compress-Archive refuses
|
|
87
|
+
# without -Force), so start from nothing.
|
|
88
|
+
FileUtils.rm_f(zip_path)
|
|
62
89
|
if Gem.win_platform?
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
env = { "OCRAN_ZIP_SOURCE" => source_dir.to_s, "OCRAN_ZIP_DESTINATION" => zip_path }
|
|
91
|
+
system(env, "powershell", "-NoProfile", "-NonInteractive", "-Command", POWERSHELL_ZIP_COMMAND,
|
|
65
92
|
exception: true)
|
|
66
93
|
else
|
|
94
|
+
# -y stores symlinks (the libruby.so aliases) as links; without it
|
|
95
|
+
# zip follows them and stores the library once per alias.
|
|
67
96
|
Dir.chdir(source_dir) do
|
|
68
|
-
system("zip", "-r", zip_path, ".", exception: true)
|
|
97
|
+
system("zip", "-q", "-r", "-y", zip_path, ".", exception: true)
|
|
69
98
|
end
|
|
70
99
|
end
|
|
71
100
|
end
|
|
@@ -84,13 +113,41 @@ module Ocran
|
|
|
84
113
|
write_launch_script
|
|
85
114
|
end
|
|
86
115
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
def
|
|
90
|
-
|
|
116
|
+
# Anchors a relative packed path at the extraction root placeholder;
|
|
117
|
+
# absolute paths are kept as they are.
|
|
118
|
+
def root_path(path)
|
|
119
|
+
return path if path.start_with?("#{EXTRACT_ROOT}/") || File.absolute_path?(path)
|
|
120
|
+
"#{EXTRACT_ROOT}/#{path}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Quotes +value+ as one POSIX shell word. The extraction root placeholder
|
|
124
|
+
# becomes "$SCRIPT_DIR"; everything else is single-quoted, so the shell
|
|
125
|
+
# expands nothing in it.
|
|
126
|
+
def shell_word(value)
|
|
127
|
+
word = value.split("#{EXTRACT_ROOT}/", -1).map { |part|
|
|
128
|
+
part.empty? ? "" : "'#{part.gsub("'", %q('"'"'))}'"
|
|
129
|
+
}.join('"$SCRIPT_DIR"/')
|
|
130
|
+
word.empty? ? "''" : word
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The batch script's reference to its own directory, which ends in a
|
|
134
|
+
# backslash; see WindowsCommandEscaping#batch_value.
|
|
135
|
+
BATCH_SCRIPT_DIR = "%SCRIPT_DIR%"
|
|
136
|
+
|
|
137
|
+
# The directory the launch script changes into before it starts the
|
|
138
|
+
# application, as a packed path (see root_path), or nil to stay put.
|
|
139
|
+
def chdir_target
|
|
140
|
+
if @chdir_exe_dir
|
|
141
|
+
EXTRACT_ROOT.to_s
|
|
142
|
+
elsif @chdir_before && @exec_args
|
|
143
|
+
dir = File.dirname(@exec_args[1])
|
|
144
|
+
dir == "." ? EXTRACT_ROOT.to_s : root_path(dir)
|
|
145
|
+
end
|
|
91
146
|
end
|
|
92
147
|
|
|
93
148
|
def script_basename
|
|
149
|
+
return @script_name.to_s if @script_name
|
|
150
|
+
|
|
94
151
|
@exec_args ? Pathname(@exec_args[1]).basename.sub_ext("").to_s : "run"
|
|
95
152
|
end
|
|
96
153
|
|
|
@@ -107,24 +164,18 @@ module Ocran
|
|
|
107
164
|
]
|
|
108
165
|
|
|
109
166
|
@env.each do |name, value|
|
|
110
|
-
|
|
111
|
-
|
|
167
|
+
lines << "export #{name}=#{shell_word(value)}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if (dir = chdir_target)
|
|
171
|
+
target = dir == EXTRACT_ROOT.to_s ? '"$SCRIPT_DIR"' : shell_word(dir)
|
|
172
|
+
lines << "cd #{target} || exit 1"
|
|
112
173
|
end
|
|
113
174
|
|
|
114
175
|
if @exec_args
|
|
115
176
|
image, script, argv = @exec_args
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# Prepend SCRIPT_DIR to relative paths
|
|
120
|
-
image_r = "$SCRIPT_DIR/#{image_r}" unless image_r.start_with?("/", "$SCRIPT_DIR")
|
|
121
|
-
script_r = "$SCRIPT_DIR/#{script_r}" unless script_r.start_with?("/", "$SCRIPT_DIR")
|
|
122
|
-
|
|
123
|
-
args = argv.map { |a| "\"#{replace_root(a, "$SCRIPT_DIR")}\"" }.join(" ")
|
|
124
|
-
exec_line = "exec \"#{image_r}\" \"#{script_r}\""
|
|
125
|
-
exec_line += " #{args}" unless args.empty?
|
|
126
|
-
exec_line += ' "$@"'
|
|
127
|
-
lines << exec_line
|
|
177
|
+
words = [root_path(image), root_path(script), *argv].map { |a| shell_word(a) }
|
|
178
|
+
lines << "exec #{words.join(" ")} \"$@\""
|
|
128
179
|
end
|
|
129
180
|
|
|
130
181
|
File.write(script_path, lines.join("\n") + "\n")
|
|
@@ -134,29 +185,28 @@ module Ocran
|
|
|
134
185
|
def write_batch_script
|
|
135
186
|
script_path = @path / "#{script_basename}.bat"
|
|
136
187
|
|
|
188
|
+
# setlocal keeps the variables and the working directory set here from
|
|
189
|
+
# outliving the script in the cmd.exe session that ran it.
|
|
137
190
|
lines = [
|
|
138
191
|
"@echo off",
|
|
139
|
-
"
|
|
192
|
+
"setlocal",
|
|
193
|
+
'set "SCRIPT_DIR=%~dp0"',
|
|
140
194
|
]
|
|
141
195
|
|
|
142
196
|
@env.each do |name, value|
|
|
143
|
-
|
|
144
|
-
|
|
197
|
+
lines << "set \"#{name}=#{batch_value(value, BATCH_SCRIPT_DIR).tr("/", "\\")}\""
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if (dir = chdir_target)
|
|
201
|
+
target = dir == EXTRACT_ROOT.to_s ? BATCH_SCRIPT_DIR : batch_value(dir, BATCH_SCRIPT_DIR).tr("/", "\\")
|
|
202
|
+
lines << "cd /d #{quote_and_escape(target)} || exit /b 1"
|
|
145
203
|
end
|
|
146
204
|
|
|
147
205
|
if @exec_args
|
|
148
206
|
image, script, argv = @exec_args
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
image_r = "%SCRIPT_DIR%#{image_r}" unless image_r.include?("%SCRIPT_DIR%") || File.absolute_path?(image_r)
|
|
153
|
-
script_r = "%SCRIPT_DIR%#{script_r}" unless script_r.include?("%SCRIPT_DIR%") || File.absolute_path?(script_r)
|
|
154
|
-
|
|
155
|
-
args = argv.map { |a| replace_root(a, "%SCRIPT_DIR%") }.join(" ")
|
|
156
|
-
exec_line = "\"#{image_r}\" \"#{script_r}\""
|
|
157
|
-
exec_line += " #{args}" unless args.empty?
|
|
158
|
-
exec_line += " %*"
|
|
159
|
-
lines << exec_line
|
|
207
|
+
words = [root_path(image), root_path(script)].map { |p| batch_value(p, BATCH_SCRIPT_DIR).tr("/", "\\") }
|
|
208
|
+
words += argv.map { |a| batch_value(a, BATCH_SCRIPT_DIR) }
|
|
209
|
+
lines << "#{words.map { |w| quote_and_escape(w) }.join(" ")} %*"
|
|
160
210
|
end
|
|
161
211
|
|
|
162
212
|
File.write(script_path, lines.join("\r\n") + "\r\n")
|
data/lib/ocran/direction.rb
CHANGED
|
@@ -16,7 +16,9 @@ module Ocran
|
|
|
16
16
|
# - /ruby/3.0.0/
|
|
17
17
|
# - /ruby/site_ruby/3.0.0/
|
|
18
18
|
# - /ruby/vendor_ruby/3.0.0/
|
|
19
|
-
|
|
19
|
+
# The version directory is RbConfig's ruby_version, which development
|
|
20
|
+
# builds suffix with their ABI version (e.g. "4.1.0+5").
|
|
21
|
+
RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+(?:\+\d+)?)/?$}i
|
|
20
22
|
|
|
21
23
|
# Core libraries provided by glibc and the dynamic loader. These must
|
|
22
24
|
# never be bundled: they are tightly coupled to the target system's
|
|
@@ -35,6 +37,9 @@ module Ocran
|
|
|
35
37
|
# payload, which is statically linked and has no dlopen.
|
|
36
38
|
NATIVE_BINARY_EXTENSIONS = %w[.so .bundle .dll].freeze
|
|
37
39
|
|
|
40
|
+
# Visual C++ 2015-2022 runtime DLLs (Microsoft.VC14x.CRT redistributables)
|
|
41
|
+
VC_RUNTIME_DLL_RE = /\A(?:vcruntime140(?:_\d+)?|msvcp140(?:_\w+)?|concrt140|vccorlib140|vcomp140)\.dll\z/i
|
|
42
|
+
|
|
38
43
|
# Native binaries a gem ships or has had built for it, i.e. the files
|
|
39
44
|
# that make it unusable under the cosmopolitan Ruby payload.
|
|
40
45
|
#
|
|
@@ -176,7 +181,48 @@ module Ocran
|
|
|
176
181
|
else
|
|
177
182
|
require_relative "library_detector_posix"
|
|
178
183
|
end
|
|
179
|
-
LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
|
|
184
|
+
dlls = LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
|
|
185
|
+
# A Ruby reached through a junction (the "current" link of rb or
|
|
186
|
+
# scoop) loads its DLLs by the junction path, while RbConfig reports
|
|
187
|
+
# the real prefix. Resolve them, or no DLL is recognised as part of
|
|
188
|
+
# the Ruby installation.
|
|
189
|
+
dlls.map! { |dll| dll.realpath rescue dll } if Gem.win_platform?
|
|
190
|
+
dlls
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The DLLs that the packed ruby.exe (or rubyw.exe), the Ruby DLL and
|
|
194
|
+
# the native extensions among +features+ import, directly or through
|
|
195
|
+
# each other, that are found in the Ruby bin directory, and the Visual
|
|
196
|
+
# C++ runtime DLLs among them (from the system directory). System DLLs,
|
|
197
|
+
# and DLLs elsewhere that only the dependency run can find, are not
|
|
198
|
+
# included.
|
|
199
|
+
def windows_imported_dlls(features)
|
|
200
|
+
require_relative "pe_imports"
|
|
201
|
+
|
|
202
|
+
bin_dlls = bindir.children.select { |path| path.extname?(".dll") && path.file? }
|
|
203
|
+
.to_h { |path| [path.basename.to_s.downcase, path] }
|
|
204
|
+
system_dir = Pathname((ENV["SystemRoot"] || "C:/Windows").tr("\\", "/")) / "System32"
|
|
205
|
+
seen = Set.new
|
|
206
|
+
seen << libruby_so.to_s.downcase if libruby_so
|
|
207
|
+
found = []
|
|
208
|
+
queue = [bindir / ruby_executable]
|
|
209
|
+
queue << bindir / libruby_so if libruby_so
|
|
210
|
+
queue += features.select { |f| f.extname?(".so") }
|
|
211
|
+
while (image = queue.shift)
|
|
212
|
+
next unless image.file?
|
|
213
|
+
|
|
214
|
+
PEImports.dll_names(image).each do |name|
|
|
215
|
+
next unless seen.add?(name.downcase)
|
|
216
|
+
|
|
217
|
+
dll = bin_dlls[name.downcase]
|
|
218
|
+
dll ||= system_dir / name if name.match?(VC_RUNTIME_DLL_RE) && (system_dir / name).file?
|
|
219
|
+
next unless dll
|
|
220
|
+
|
|
221
|
+
found << dll
|
|
222
|
+
queue << dll
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
found
|
|
180
226
|
end
|
|
181
227
|
|
|
182
228
|
def find_gemspecs(features)
|
|
@@ -191,8 +237,7 @@ module Ocran
|
|
|
191
237
|
end
|
|
192
238
|
end
|
|
193
239
|
if defined?(Gem)
|
|
194
|
-
|
|
195
|
-
specs += Gem.loaded_specs.each_value.reject { |spec| foreign.include?(spec.name) }
|
|
240
|
+
specs += application_loaded_specs
|
|
196
241
|
# Now, we also detect gems that are not included in Gem.loaded_specs.
|
|
197
242
|
# Therefore, we look for any loaded file from a gem path.
|
|
198
243
|
specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
|
|
@@ -202,6 +247,46 @@ module Ocran
|
|
|
202
247
|
specs
|
|
203
248
|
end
|
|
204
249
|
|
|
250
|
+
# The gems RubyGems had activated when the dependency run ended, less
|
|
251
|
+
# those that are not the application's.
|
|
252
|
+
#
|
|
253
|
+
# Read from the snapshot taken with the loaded features, not from
|
|
254
|
+
# Gem.loaded_specs, which by now also holds what OCRAN activated for the
|
|
255
|
+
# build itself - fiddle for DLL detection, tempfile with tmpdir,
|
|
256
|
+
# fileutils, delegate and etc for the builder - and which every
|
|
257
|
+
# application used to get packed since 1.3.16 moved the build ahead of
|
|
258
|
+
# the gem scan. OCRAN's own gem and its runtime dependencies (fiddle) are
|
|
259
|
+
# dropped as well: the `ocran` command activates them before the
|
|
260
|
+
# dependency run starts, and since OCRA 1.3.2 they were packed into
|
|
261
|
+
# every application built from an installed OCRAN, although nothing in a
|
|
262
|
+
# packaged application loads OCRAN. Only activation is discounted: an
|
|
263
|
+
# application that does load one of them still gets it packed through
|
|
264
|
+
# detect_gems_from.
|
|
265
|
+
def application_loaded_specs
|
|
266
|
+
@application_loaded_specs ||= begin
|
|
267
|
+
excluded = foreign_bundle_gem_names | ocran_gem_names
|
|
268
|
+
@post_env.loaded_specs.reject { |spec| excluded.include?(spec.name) }
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The names of the gem this OCRAN runs from and of the gems it activated
|
|
273
|
+
# as its runtime dependencies. Empty when OCRAN does not run from an
|
|
274
|
+
# installed gem, as from a source checkout.
|
|
275
|
+
def ocran_gem_names
|
|
276
|
+
specs = @post_env.loaded_specs.to_h { |spec| [spec.name, spec] }
|
|
277
|
+
own = specs.each_value.find { |spec| Pathname(__dir__).subpath?(spec.gem_dir) }
|
|
278
|
+
names = Set.new
|
|
279
|
+
queue = own ? [own] : []
|
|
280
|
+
while (spec = queue.shift)
|
|
281
|
+
next unless names.add?(spec.name)
|
|
282
|
+
|
|
283
|
+
spec.runtime_dependencies.each do |dep|
|
|
284
|
+
queue << specs[dep.name] if specs.key?(dep.name)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
names
|
|
288
|
+
end
|
|
289
|
+
|
|
205
290
|
# The gems RubyGems had already activated for a bundle that is not the
|
|
206
291
|
# application's, by the time OCRAN started.
|
|
207
292
|
#
|
|
@@ -445,8 +530,17 @@ module Ocran
|
|
|
445
530
|
end
|
|
446
531
|
end
|
|
447
532
|
|
|
533
|
+
# Windows-only: DLLs bundled into bin, next to ruby.exe, by basename
|
|
534
|
+
dlls_in_bin = Set.new
|
|
535
|
+
add_dll_to_bin = proc do |dll|
|
|
536
|
+
next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
|
|
537
|
+
|
|
538
|
+
say "Adding detected DLL #{dll} to bin"
|
|
539
|
+
builder.copy_to_bin(dll, dll.basename)
|
|
540
|
+
end
|
|
541
|
+
|
|
448
542
|
# Windows-only: Add detected DLLs
|
|
449
|
-
if Gem.win_platform? && @option.auto_detect_dlls?
|
|
543
|
+
if Gem.win_platform? && @option.auto_detect_dlls? && !@option.static_runtime?
|
|
450
544
|
# The Windows loader resolves the imports of a native extension from
|
|
451
545
|
# the extension's own directory, the application directory of the
|
|
452
546
|
# packed ruby.exe (bin) plus its SxS assembly (ruby_builtin_dlls),
|
|
@@ -463,16 +557,17 @@ module Ocran
|
|
|
463
557
|
# LD_LIBRARY_PATH. DLLs from the Windows directory always come from
|
|
464
558
|
# the target system and are never bundled.
|
|
465
559
|
windows_dir = Pathname(ENV["SystemRoot"] || "C:/Windows")
|
|
466
|
-
dlls_in_bin = Set.new
|
|
467
|
-
add_dll_to_bin = proc do |dll|
|
|
468
|
-
next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
|
|
469
|
-
|
|
470
|
-
say "Adding detected DLL #{dll} to bin"
|
|
471
|
-
builder.copy_to_bin(dll, dll.basename)
|
|
472
|
-
end
|
|
473
560
|
|
|
474
561
|
detect_dlls.each do |dll|
|
|
475
562
|
next unless dll.extname?(".dll") && dll.basename != libruby_so
|
|
563
|
+
# The Visual C++ runtime is not part of Windows: an mswin Ruby
|
|
564
|
+
# (and any MSVC-built DLL a gem loads) fails to start with
|
|
565
|
+
# 0xC0000135 where the VC++ Redistributable is not installed.
|
|
566
|
+
# Microsoft permits deploying it app-locally, next to ruby.exe.
|
|
567
|
+
if dll.basename.to_s.match?(VC_RUNTIME_DLL_RE)
|
|
568
|
+
add_dll_to_bin.call(dll)
|
|
569
|
+
next
|
|
570
|
+
end
|
|
476
571
|
next if dll.subpath?(windows_dir)
|
|
477
572
|
|
|
478
573
|
if dll.subpath?(exec_prefix)
|
|
@@ -492,7 +587,7 @@ module Ocran
|
|
|
492
587
|
# the same extension packed at a gem path (openssl and psych are
|
|
493
588
|
# gems since Ruby 3.x) resolves its imports from bin.
|
|
494
589
|
features.select { |f| f.extname?(".so") && f.subpath?(exec_prefix) }
|
|
495
|
-
.map(&:dirname).uniq
|
|
590
|
+
.map(&:dirname).uniq { |dir| dir.path_key }
|
|
496
591
|
.each do |dir|
|
|
497
592
|
dir.each_child do |path|
|
|
498
593
|
next unless path.file? && path.extname?(".dll")
|
|
@@ -503,6 +598,25 @@ module Ocran
|
|
|
503
598
|
end
|
|
504
599
|
end
|
|
505
600
|
|
|
601
|
+
# Windows-only: DLLs that ruby.exe, the Ruby DLL and the packed native
|
|
602
|
+
# extensions import from the Ruby bin directory, plus the Visual C++
|
|
603
|
+
# runtime. RubyInstaller keeps such DLLs in ruby_builtin_dlls, which is
|
|
604
|
+
# packed whole below; an mswin Ruby keeps them (gmp, libffi, OpenSSL,
|
|
605
|
+
# libyaml, zlib from vcpkg) next to ruby.exe, and its interpreter does
|
|
606
|
+
# not even start without gmp and vcruntime140. They are read from the
|
|
607
|
+
# PE import tables, so they are packed with --no-autodll too, and for
|
|
608
|
+
# extensions the dependency run did not exercise.
|
|
609
|
+
if Gem.win_platform?
|
|
610
|
+
windows_imported_dlls(features).each do |dll|
|
|
611
|
+
if dll.subpath?(bindir)
|
|
612
|
+
verbose "Adding imported DLL #{dll}"
|
|
613
|
+
builder.duplicate_to_exec_prefix(dll)
|
|
614
|
+
else
|
|
615
|
+
add_dll_to_bin.call(dll)
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
506
620
|
# Linux: bundle detected shared libraries (e.g. libyaml, libssl,
|
|
507
621
|
# libcrypt) next to libruby so the executable also runs on systems
|
|
508
622
|
# where they are missing or have different sonames (the stub already
|
|
@@ -510,7 +624,7 @@ module Ocran
|
|
|
510
624
|
# libraries and the loader are never bundled - they must come from the
|
|
511
625
|
# target system. Ruby native extensions are packed as features, not
|
|
512
626
|
# here. Not needed with --cosmo-ruby: the APE payload is static.
|
|
513
|
-
if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls? && !@option.cosmo_ruby
|
|
627
|
+
if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls? && !@option.cosmo_ruby && !@option.static_runtime?
|
|
514
628
|
feature_set = features.to_set
|
|
515
629
|
feature_realpaths = features.filter_map { |f| f.realpath rescue nil }.to_set
|
|
516
630
|
# Ruby native extensions live in these directories and are packed as
|
|
@@ -560,7 +674,7 @@ module Ocran
|
|
|
560
674
|
archdir = Pathname(RbConfig::CONFIG["archdir"])
|
|
561
675
|
sxs_manifest_dirs << archdir if archdir.exist? && archdir.subpath?(exec_prefix)
|
|
562
676
|
if defined?(Gem)
|
|
563
|
-
|
|
677
|
+
application_loaded_specs.each do |spec|
|
|
564
678
|
next if spec.extensions.empty?
|
|
565
679
|
ext_dir = Pathname(spec.extension_dir)
|
|
566
680
|
sxs_manifest_dirs << ext_dir if ext_dir.exist? && ext_dir.subpath?(exec_prefix)
|
|
@@ -812,7 +926,9 @@ module Ocran
|
|
|
812
926
|
|
|
813
927
|
actual_files
|
|
814
928
|
end
|
|
815
|
-
|
|
929
|
+
# Keyed on Pathname#path_key here and below: Array#uniq and Set do not
|
|
930
|
+
# see the refined, case-insensitive Pathname#eql? on Windows.
|
|
931
|
+
gem_files.uniq! { |file| file.path_key }
|
|
816
932
|
|
|
817
933
|
# On some distros parts of a gem are reachable through symlinks at other
|
|
818
934
|
# locations (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the
|
|
@@ -820,16 +936,17 @@ module Ocran
|
|
|
820
936
|
# the same file under different paths. Compare realpaths when removing
|
|
821
937
|
# gem files from the feature list; otherwise a stdlib-level duplicate
|
|
822
938
|
# would be packed as well and shadow the packed gem at runtime.
|
|
823
|
-
gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil })
|
|
939
|
+
gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil })
|
|
940
|
+
.to_set { |file| file.path_key }
|
|
824
941
|
features = features.reject do |feature|
|
|
825
|
-
next true if gem_file_set.include?(feature)
|
|
942
|
+
next true if gem_file_set.include?(feature.path_key)
|
|
826
943
|
|
|
827
944
|
real = begin
|
|
828
945
|
feature.realpath
|
|
829
946
|
rescue SystemCallError
|
|
830
947
|
nil
|
|
831
948
|
end
|
|
832
|
-
real && gem_file_set.include?(real)
|
|
949
|
+
real && gem_file_set.include?(real.path_key)
|
|
833
950
|
end
|
|
834
951
|
|
|
835
952
|
# If requested, add all ruby standard libraries
|
|
@@ -844,7 +961,6 @@ module Ocran
|
|
|
844
961
|
end
|
|
845
962
|
path.find.each do |src|
|
|
846
963
|
next if src.directory?
|
|
847
|
-
a = Pathname(subdir) / src.relative_path_from(path)
|
|
848
964
|
builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
|
|
849
965
|
end
|
|
850
966
|
end
|
|
@@ -873,7 +989,8 @@ module Ocran
|
|
|
873
989
|
end
|
|
874
990
|
|
|
875
991
|
# Windows-only: Workaround for RubyInstaller MSYS folder detection
|
|
876
|
-
|
|
992
|
+
# (mswin Ruby, built with Visual C++, has no MSYS2 devkit)
|
|
993
|
+
if Gem.win_platform? && !RUBY_PLATFORM.include?("mswin")
|
|
877
994
|
# RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present
|
|
878
995
|
# (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
|
|
879
996
|
builder.touch('msys64/usr/bin/msys-2.0.dll')
|
|
@@ -1043,7 +1160,7 @@ module Ocran
|
|
|
1043
1160
|
neutralize_bundler_env(builder)
|
|
1044
1161
|
# Add the load path that are required with the correct path after
|
|
1045
1162
|
# src_prefix was adjusted.
|
|
1046
|
-
load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
|
|
1163
|
+
load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq { |path| path.path_key }
|
|
1047
1164
|
|
|
1048
1165
|
# On POSIX systems, also add the packed Ruby standard library directories
|
|
1049
1166
|
# to RUBYLIB. The Ruby binary has a compiled-in prefix pointing to the build
|
|
@@ -1082,7 +1199,7 @@ module Ocran
|
|
|
1082
1199
|
prefix_gem_dirs = (Gem.path.map { |dir| Pathname(dir) } + [Pathname(Gem.default_dir)])
|
|
1083
1200
|
.select { |dir| dir.subpath?(exec_prefix) }
|
|
1084
1201
|
.map { |dir| dir.relative_path_from(exec_prefix) }
|
|
1085
|
-
.uniq
|
|
1202
|
+
.uniq { |dir| dir.path_key }
|
|
1086
1203
|
gem_paths += prefix_gem_dirs
|
|
1087
1204
|
# RubyGems probes the default gem dir for writability at startup and
|
|
1088
1205
|
# prints "Can't determine writability of default gem path" on stderr
|
|
@@ -1256,6 +1373,20 @@ module Ocran
|
|
|
1256
1373
|
end
|
|
1257
1374
|
end
|
|
1258
1375
|
|
|
1376
|
+
# The PE resource options (version info, string table, manifest), for
|
|
1377
|
+
# every Windows executable the user launches: the self-extracting exe and
|
|
1378
|
+
# the wrapper that installer, directory and zip output start from.
|
|
1379
|
+
def pe_resource_options
|
|
1380
|
+
{
|
|
1381
|
+
version_strings: @option.version_strings,
|
|
1382
|
+
file_version: @option.file_version,
|
|
1383
|
+
product_version: @option.product_version,
|
|
1384
|
+
resource_strings: @option.resource_strings,
|
|
1385
|
+
execution_level: @option.execution_level,
|
|
1386
|
+
application_manifest: @option.application_manifest&.to_s,
|
|
1387
|
+
}
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1259
1390
|
# Builds the small RUN_IN_EXE_DIR wrapper stub that starts the deployed
|
|
1260
1391
|
# application directly from the directory the wrapper resides in.
|
|
1261
1392
|
def build_wrapper_exe(wrapper_path)
|
|
@@ -1268,7 +1399,8 @@ module Ocran
|
|
|
1268
1399
|
gui_mode: @option.windowed?,
|
|
1269
1400
|
icon_path: @option.icon_filename,
|
|
1270
1401
|
run_in_exe_dir: true,
|
|
1271
|
-
stub_path: cosmo_stub_path
|
|
1402
|
+
stub_path: cosmo_stub_path,
|
|
1403
|
+
**pe_resource_options) do |stub|
|
|
1272
1404
|
yield(stub)
|
|
1273
1405
|
end
|
|
1274
1406
|
end
|
|
@@ -1278,7 +1410,11 @@ module Ocran
|
|
|
1278
1410
|
|
|
1279
1411
|
path = Pathname(path)
|
|
1280
1412
|
say "Building directory #{path}"
|
|
1281
|
-
builder = DirBuilder.new(path,
|
|
1413
|
+
builder = DirBuilder.new(path,
|
|
1414
|
+
script_name: @option.script.basename.sub_ext("").to_s,
|
|
1415
|
+
chdir_before: @option.chdir_before?,
|
|
1416
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1417
|
+
&to_proc)
|
|
1282
1418
|
|
|
1283
1419
|
if @option.wrapper_exe?
|
|
1284
1420
|
# Same wrapper as in Inno Setup builds: a doubleclickable executable
|
|
@@ -1342,30 +1478,41 @@ module Ocran
|
|
|
1342
1478
|
end
|
|
1343
1479
|
|
|
1344
1480
|
bundle_id = @option.bundle_identifier || "com.example.#{app_name}"
|
|
1345
|
-
|
|
1481
|
+
File.write(contents_dir / "Info.plist",
|
|
1482
|
+
self.class.info_plist(app_name, bundle_id, icon: !!@option.icon_filename))
|
|
1346
1483
|
|
|
1347
|
-
|
|
1484
|
+
say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
XML_ESCAPES = { "&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'" }.freeze
|
|
1488
|
+
|
|
1489
|
+
# The Info.plist of a macOS app bundle. The values are XML-escaped: an
|
|
1490
|
+
# app named "R&D" must not produce a property list macOS cannot parse.
|
|
1491
|
+
def self.info_plist(app_name, bundle_id, icon: false)
|
|
1492
|
+
name = app_name.to_s.gsub(/[&<>"']/, XML_ESCAPES)
|
|
1493
|
+
id = bundle_id.to_s.gsub(/[&<>"']/, XML_ESCAPES)
|
|
1494
|
+
icon_entry = icon ? " <key>CFBundleIconFile</key>\n <string>AppIcon</string>\n" : ""
|
|
1495
|
+
|
|
1496
|
+
<<~PLIST
|
|
1348
1497
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1349
1498
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1350
1499
|
<plist version="1.0">
|
|
1351
1500
|
<dict>
|
|
1352
1501
|
<key>CFBundleName</key>
|
|
1353
|
-
<string>#{
|
|
1502
|
+
<string>#{name}</string>
|
|
1354
1503
|
<key>CFBundleDisplayName</key>
|
|
1355
|
-
<string>#{
|
|
1504
|
+
<string>#{name}</string>
|
|
1356
1505
|
<key>CFBundleIdentifier</key>
|
|
1357
|
-
<string>#{
|
|
1506
|
+
<string>#{id}</string>
|
|
1358
1507
|
<key>CFBundleVersion</key>
|
|
1359
1508
|
<string>1.0</string>
|
|
1360
1509
|
<key>CFBundlePackageType</key>
|
|
1361
1510
|
<string>APPL</string>
|
|
1362
1511
|
<key>CFBundleExecutable</key>
|
|
1363
|
-
<string>#{
|
|
1512
|
+
<string>#{name}</string>
|
|
1364
1513
|
#{icon_entry}</dict>
|
|
1365
1514
|
</plist>
|
|
1366
1515
|
PLIST
|
|
1367
|
-
|
|
1368
|
-
say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
|
|
1369
1516
|
end
|
|
1370
1517
|
|
|
1371
1518
|
# Builds the executable by copying the cosmopolitan Ruby and injecting
|
|
@@ -1380,6 +1527,7 @@ module Ocran
|
|
|
1380
1527
|
ZipPayloadBuilder.new(output,
|
|
1381
1528
|
cosmo_ruby: @option.cosmo_ruby,
|
|
1382
1529
|
chdir_before: @option.chdir_before?,
|
|
1530
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1383
1531
|
debug_mode: @option.enable_debug_mode?,
|
|
1384
1532
|
&to_proc) => builder
|
|
1385
1533
|
|
|
@@ -1402,6 +1550,56 @@ module Ocran
|
|
|
1402
1550
|
say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
|
|
1403
1551
|
end
|
|
1404
1552
|
|
|
1553
|
+
# --static-runtime: the application appended as a ZIP archive to a statically
|
|
1554
|
+
# linked Windows Ruby (see StaticRuntimeBuilder).
|
|
1555
|
+
def build_static_runtime_exe
|
|
1556
|
+
require_relative "static_runtime_builder"
|
|
1557
|
+
|
|
1558
|
+
runtime = StaticRuntime.load(@option.static_runtime)
|
|
1559
|
+
runtime.verify!
|
|
1560
|
+
host_api = RbConfig::CONFIG["ruby_version"]
|
|
1561
|
+
# Gems, default gem specs and the host files the runtime does not
|
|
1562
|
+
# replace were resolved for the build host's Ruby; they only fit a
|
|
1563
|
+
# runtime with the same API version.
|
|
1564
|
+
unless host_api == runtime.ruby_api_version
|
|
1565
|
+
raise "The static runtime in #{runtime.dir} is Ruby #{runtime.ruby_version} (API #{runtime.ruby_api_version}), " \
|
|
1566
|
+
"but this is Ruby #{RUBY_VERSION} (API #{host_api}): package with a Ruby #{runtime.ruby_api_version.sub(/\.0\z/, "")}"
|
|
1567
|
+
end
|
|
1568
|
+
if RUBY_VERSION != runtime.ruby_version
|
|
1569
|
+
warning "Dependency detection ran under Ruby #{RUBY_VERSION}; the static runtime is Ruby #{runtime.ruby_version}"
|
|
1570
|
+
end
|
|
1571
|
+
say "Packaging for the static runtime #{runtime.executable} (Ruby #{runtime.ruby_version} #{runtime.arch})"
|
|
1572
|
+
if @option.extra_dlls.any?
|
|
1573
|
+
warning "--dll has no effect with --static-runtime: a DLL cannot be loaded from inside the executable"
|
|
1574
|
+
end
|
|
1575
|
+
|
|
1576
|
+
output = @option.output_executable
|
|
1577
|
+
StaticRuntimeBuilder.new(output,
|
|
1578
|
+
runtime: runtime,
|
|
1579
|
+
host_ruby_api_version: host_api,
|
|
1580
|
+
host_arch: RbConfig::CONFIG["arch"],
|
|
1581
|
+
chdir_before: @option.chdir_before?,
|
|
1582
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1583
|
+
windowed: @option.windowed?,
|
|
1584
|
+
debug_mode: @option.enable_debug_mode?,
|
|
1585
|
+
dlls: @option.runtime_dlls,
|
|
1586
|
+
resources: { icon_path: @option.icon_filename&.to_s, **pe_resource_options },
|
|
1587
|
+
&to_proc) => builder
|
|
1588
|
+
|
|
1589
|
+
builder.skipped.each do |reason, names|
|
|
1590
|
+
verbose "Not packed (#{reason.to_s.tr("_", " ")}): #{names.size} file(s)"
|
|
1591
|
+
names.each { |name| verbose "\t#{name}" }
|
|
1592
|
+
end
|
|
1593
|
+
unless builder.skipped[:not_in_runtime_stdlib].empty?
|
|
1594
|
+
say "#{builder.skipped[:not_in_runtime_stdlib].size} file(s) of this Ruby's standard library are not " \
|
|
1595
|
+
"in the runtime's and were left out (--verbose lists them)"
|
|
1596
|
+
end
|
|
1597
|
+
say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
|
|
1598
|
+
unless builder.shipped_dlls.empty?
|
|
1599
|
+
say "Ship these DLLs next to #{output.basename}: #{builder.shipped_dlls.map { |d| d.basename.to_s }.join(", ")}"
|
|
1600
|
+
end
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1405
1603
|
def build_stab_exe
|
|
1406
1604
|
require_relative "stub_builder"
|
|
1407
1605
|
|
|
@@ -1418,6 +1616,7 @@ module Ocran
|
|
|
1418
1616
|
gui_mode: @option.windowed?,
|
|
1419
1617
|
icon_path: @option.icon_filename,
|
|
1420
1618
|
stub_path: cosmo_stub_path,
|
|
1619
|
+
**pe_resource_options,
|
|
1421
1620
|
&to_proc) => builder
|
|
1422
1621
|
say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
|
|
1423
1622
|
say "After decompression, the data will expand to #{builder.data_size} bytes."
|