ocran 1.4.6 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.txt +68 -0
  3. data/README.md +295 -10
  4. data/ext/extconf.rb +5 -1
  5. data/lib/ocran/aot_toolchain.rb +154 -0
  6. data/lib/ocran/command_output.rb +8 -1
  7. data/lib/ocran/compatibility_scan.rb +414 -0
  8. data/lib/ocran/cosmo_toolchain.rb +29 -7
  9. data/lib/ocran/dir_builder.rb +88 -38
  10. data/lib/ocran/direction.rb +190 -30
  11. data/lib/ocran/ed_resource.rb +113 -0
  12. data/lib/ocran/ed_resource_builder.rb +199 -0
  13. data/lib/ocran/file_path_set.rb +9 -5
  14. data/lib/ocran/gem_spec_queryable.rb +7 -3
  15. data/lib/ocran/host_config_helper.rb +11 -1
  16. data/lib/ocran/inno_setup_script_builder.rb +24 -14
  17. data/lib/ocran/launcher_batch_builder.rb +8 -11
  18. data/lib/ocran/npm_package.rb +106 -0
  19. data/lib/ocran/option.rb +425 -35
  20. data/lib/ocran/pe_imports.rb +96 -0
  21. data/lib/ocran/pe_resources.rb +226 -0
  22. data/lib/ocran/refine_pathname.rb +29 -32
  23. data/lib/ocran/roundhouse_builder.rb +316 -0
  24. data/lib/ocran/runner.rb +43 -11
  25. data/lib/ocran/runtime_sdk.rb +252 -0
  26. data/lib/ocran/spinel_builder.rb +183 -0
  27. data/lib/ocran/spinel_compatibility.rb +149 -0
  28. data/lib/ocran/static_runtime/boot.rb +443 -0
  29. data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
  30. data/lib/ocran/static_runtime_builder.rb +814 -0
  31. data/lib/ocran/stub_builder.rb +70 -25
  32. data/lib/ocran/version.rb +1 -1
  33. data/lib/ocran/wasm_builder.rb +570 -0
  34. data/lib/ocran/wasm_compatibility.rb +198 -0
  35. data/lib/ocran/windows_command_escaping.rb +21 -0
  36. data/lib/ocran/zip_payload_builder.rb +18 -9
  37. data/lib/ocran/zip_writer.rb +48 -9
  38. data/src/Makefile +18 -2
  39. data/src/Makefile.mswin +65 -0
  40. data/src/inst_dir.c +34 -6
  41. data/src/path_utils.c +144 -0
  42. data/src/script_info.c +5 -0
  43. data/src/stub.c +77 -19
  44. data/src/stub.rc +1 -1
  45. data/src/system_utils.c +276 -155
  46. data/src/system_utils.h +46 -3
  47. data/src/system_utils_posix.c +243 -148
  48. data/src/unpack.c +9 -0
  49. metadata +22 -4
@@ -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
- def initialize(path)
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
- # Create a zip archive from a source directory.
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
- system("powershell", "-NoProfile", "-Command",
64
- "Compress-Archive -Path '#{source_dir}\\*' -DestinationPath '#{zip_path}'",
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
- # Replace the EXTRACT_ROOT placeholder ("|") in a value string with
88
- # the runtime directory variable.
89
- def replace_root(value, root_var)
90
- value.gsub("#{EXTRACT_ROOT}/", "#{root_var}/")
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
- replaced = replace_root(value, "$SCRIPT_DIR")
111
- lines << "export #{name}=\"#{replaced}\""
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
- image_r = replace_root(image, "$SCRIPT_DIR")
117
- script_r = replace_root(script, "$SCRIPT_DIR")
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
- "set SCRIPT_DIR=%~dp0",
192
+ "setlocal",
193
+ 'set "SCRIPT_DIR=%~dp0"',
140
194
  ]
141
195
 
142
196
  @env.each do |name, value|
143
- replaced = replace_root(value, "%SCRIPT_DIR%").tr("/", "\\")
144
- lines << "set #{name}=#{replaced}"
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
- image_r = replace_root(image, "%SCRIPT_DIR%").tr("/", "\\")
150
- script_r = replace_root(script, "%SCRIPT_DIR%").tr("/", "\\")
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")
@@ -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
- RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+)/?$}i
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)
@@ -484,8 +530,17 @@ module Ocran
484
530
  end
485
531
  end
486
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
+
487
542
  # Windows-only: Add detected DLLs
488
- if Gem.win_platform? && @option.auto_detect_dlls?
543
+ if Gem.win_platform? && @option.auto_detect_dlls? && !@option.static_runtime?
489
544
  # The Windows loader resolves the imports of a native extension from
490
545
  # the extension's own directory, the application directory of the
491
546
  # packed ruby.exe (bin) plus its SxS assembly (ruby_builtin_dlls),
@@ -502,16 +557,17 @@ module Ocran
502
557
  # LD_LIBRARY_PATH. DLLs from the Windows directory always come from
503
558
  # the target system and are never bundled.
504
559
  windows_dir = Pathname(ENV["SystemRoot"] || "C:/Windows")
505
- dlls_in_bin = Set.new
506
- add_dll_to_bin = proc do |dll|
507
- next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
508
-
509
- say "Adding detected DLL #{dll} to bin"
510
- builder.copy_to_bin(dll, dll.basename)
511
- end
512
560
 
513
561
  detect_dlls.each do |dll|
514
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
515
571
  next if dll.subpath?(windows_dir)
516
572
 
517
573
  if dll.subpath?(exec_prefix)
@@ -531,7 +587,7 @@ module Ocran
531
587
  # the same extension packed at a gem path (openssl and psych are
532
588
  # gems since Ruby 3.x) resolves its imports from bin.
533
589
  features.select { |f| f.extname?(".so") && f.subpath?(exec_prefix) }
534
- .map(&:dirname).uniq
590
+ .map(&:dirname).uniq { |dir| dir.path_key }
535
591
  .each do |dir|
536
592
  dir.each_child do |path|
537
593
  next unless path.file? && path.extname?(".dll")
@@ -542,6 +598,25 @@ module Ocran
542
598
  end
543
599
  end
544
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
+
545
620
  # Linux: bundle detected shared libraries (e.g. libyaml, libssl,
546
621
  # libcrypt) next to libruby so the executable also runs on systems
547
622
  # where they are missing or have different sonames (the stub already
@@ -549,7 +624,7 @@ module Ocran
549
624
  # libraries and the loader are never bundled - they must come from the
550
625
  # target system. Ruby native extensions are packed as features, not
551
626
  # here. Not needed with --cosmo-ruby: the APE payload is static.
552
- 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?
553
628
  feature_set = features.to_set
554
629
  feature_realpaths = features.filter_map { |f| f.realpath rescue nil }.to_set
555
630
  # Ruby native extensions live in these directories and are packed as
@@ -851,7 +926,9 @@ module Ocran
851
926
 
852
927
  actual_files
853
928
  end
854
- gem_files.uniq!
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 }
855
932
 
856
933
  # On some distros parts of a gem are reachable through symlinks at other
857
934
  # locations (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the
@@ -859,16 +936,17 @@ module Ocran
859
936
  # the same file under different paths. Compare realpaths when removing
860
937
  # gem files from the feature list; otherwise a stdlib-level duplicate
861
938
  # would be packed as well and shadow the packed gem at runtime.
862
- gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil }).to_set
939
+ gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil })
940
+ .to_set { |file| file.path_key }
863
941
  features = features.reject do |feature|
864
- next true if gem_file_set.include?(feature)
942
+ next true if gem_file_set.include?(feature.path_key)
865
943
 
866
944
  real = begin
867
945
  feature.realpath
868
946
  rescue SystemCallError
869
947
  nil
870
948
  end
871
- real && gem_file_set.include?(real)
949
+ real && gem_file_set.include?(real.path_key)
872
950
  end
873
951
 
874
952
  # If requested, add all ruby standard libraries
@@ -883,7 +961,6 @@ module Ocran
883
961
  end
884
962
  path.find.each do |src|
885
963
  next if src.directory?
886
- a = Pathname(subdir) / src.relative_path_from(path)
887
964
  builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
888
965
  end
889
966
  end
@@ -912,7 +989,8 @@ module Ocran
912
989
  end
913
990
 
914
991
  # Windows-only: Workaround for RubyInstaller MSYS folder detection
915
- if Gem.win_platform?
992
+ # (mswin Ruby, built with Visual C++, has no MSYS2 devkit)
993
+ if Gem.win_platform? && !RUBY_PLATFORM.include?("mswin")
916
994
  # RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present
917
995
  # (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
918
996
  builder.touch('msys64/usr/bin/msys-2.0.dll')
@@ -1082,7 +1160,7 @@ module Ocran
1082
1160
  neutralize_bundler_env(builder)
1083
1161
  # Add the load path that are required with the correct path after
1084
1162
  # src_prefix was adjusted.
1085
- 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 }
1086
1164
 
1087
1165
  # On POSIX systems, also add the packed Ruby standard library directories
1088
1166
  # to RUBYLIB. The Ruby binary has a compiled-in prefix pointing to the build
@@ -1121,7 +1199,7 @@ module Ocran
1121
1199
  prefix_gem_dirs = (Gem.path.map { |dir| Pathname(dir) } + [Pathname(Gem.default_dir)])
1122
1200
  .select { |dir| dir.subpath?(exec_prefix) }
1123
1201
  .map { |dir| dir.relative_path_from(exec_prefix) }
1124
- .uniq
1202
+ .uniq { |dir| dir.path_key }
1125
1203
  gem_paths += prefix_gem_dirs
1126
1204
  # RubyGems probes the default gem dir for writability at startup and
1127
1205
  # prints "Can't determine writability of default gem path" on stderr
@@ -1295,6 +1373,20 @@ module Ocran
1295
1373
  end
1296
1374
  end
1297
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
+
1298
1390
  # Builds the small RUN_IN_EXE_DIR wrapper stub that starts the deployed
1299
1391
  # application directly from the directory the wrapper resides in.
1300
1392
  def build_wrapper_exe(wrapper_path)
@@ -1307,7 +1399,8 @@ module Ocran
1307
1399
  gui_mode: @option.windowed?,
1308
1400
  icon_path: @option.icon_filename,
1309
1401
  run_in_exe_dir: true,
1310
- stub_path: cosmo_stub_path) do |stub|
1402
+ stub_path: cosmo_stub_path,
1403
+ **pe_resource_options) do |stub|
1311
1404
  yield(stub)
1312
1405
  end
1313
1406
  end
@@ -1317,7 +1410,11 @@ module Ocran
1317
1410
 
1318
1411
  path = Pathname(path)
1319
1412
  say "Building directory #{path}"
1320
- builder = DirBuilder.new(path, &to_proc)
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)
1321
1418
 
1322
1419
  if @option.wrapper_exe?
1323
1420
  # Same wrapper as in Inno Setup builds: a doubleclickable executable
@@ -1381,30 +1478,41 @@ module Ocran
1381
1478
  end
1382
1479
 
1383
1480
  bundle_id = @option.bundle_identifier || "com.example.#{app_name}"
1384
- icon_entry = @option.icon_filename ? " <key>CFBundleIconFile</key>\n <string>AppIcon</string>\n" : ""
1481
+ File.write(contents_dir / "Info.plist",
1482
+ self.class.info_plist(app_name, bundle_id, icon: !!@option.icon_filename))
1483
+
1484
+ say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
1485
+ end
1385
1486
 
1386
- File.write(contents_dir / "Info.plist", <<~PLIST)
1487
+ XML_ESCAPES = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;", "'" => "&apos;" }.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
1387
1497
  <?xml version="1.0" encoding="UTF-8"?>
1388
1498
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1389
1499
  <plist version="1.0">
1390
1500
  <dict>
1391
1501
  <key>CFBundleName</key>
1392
- <string>#{app_name}</string>
1502
+ <string>#{name}</string>
1393
1503
  <key>CFBundleDisplayName</key>
1394
- <string>#{app_name}</string>
1504
+ <string>#{name}</string>
1395
1505
  <key>CFBundleIdentifier</key>
1396
- <string>#{bundle_id}</string>
1506
+ <string>#{id}</string>
1397
1507
  <key>CFBundleVersion</key>
1398
1508
  <string>1.0</string>
1399
1509
  <key>CFBundlePackageType</key>
1400
1510
  <string>APPL</string>
1401
1511
  <key>CFBundleExecutable</key>
1402
- <string>#{app_name}</string>
1512
+ <string>#{name}</string>
1403
1513
  #{icon_entry}</dict>
1404
1514
  </plist>
1405
1515
  PLIST
1406
-
1407
- say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
1408
1516
  end
1409
1517
 
1410
1518
  # Builds the executable by copying the cosmopolitan Ruby and injecting
@@ -1419,6 +1527,7 @@ module Ocran
1419
1527
  ZipPayloadBuilder.new(output,
1420
1528
  cosmo_ruby: @option.cosmo_ruby,
1421
1529
  chdir_before: @option.chdir_before?,
1530
+ chdir_exe_dir: @option.chdir_exe_dir?,
1422
1531
  debug_mode: @option.enable_debug_mode?,
1423
1532
  &to_proc) => builder
1424
1533
 
@@ -1441,6 +1550,56 @@ module Ocran
1441
1550
  say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
1442
1551
  end
1443
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
+
1444
1603
  def build_stab_exe
1445
1604
  require_relative "stub_builder"
1446
1605
 
@@ -1457,6 +1616,7 @@ module Ocran
1457
1616
  gui_mode: @option.windowed?,
1458
1617
  icon_path: @option.icon_filename,
1459
1618
  stub_path: cosmo_stub_path,
1619
+ **pe_resource_options,
1460
1620
  &to_proc) => builder
1461
1621
  say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
1462
1622
  say "After decompression, the data will expand to #{builder.data_size} bytes."