ocran 1.4.6 → 1.5.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.txt +82 -0
  3. data/README.md +519 -15
  4. data/ext/extconf.rb +5 -1
  5. data/lib/ocran/aot_toolchain.rb +154 -0
  6. data/lib/ocran/build_helper.rb +7 -0
  7. data/lib/ocran/command_output.rb +8 -1
  8. data/lib/ocran/compatibility_scan.rb +414 -0
  9. data/lib/ocran/cosmo_toolchain.rb +29 -7
  10. data/lib/ocran/dir_builder.rb +88 -38
  11. data/lib/ocran/direction.rb +292 -44
  12. data/lib/ocran/ed_resource.rb +113 -0
  13. data/lib/ocran/ed_resource_builder.rb +199 -0
  14. data/lib/ocran/env_hook.rb +98 -0
  15. data/lib/ocran/file_path_set.rb +9 -5
  16. data/lib/ocran/gem_spec_queryable.rb +13 -5
  17. data/lib/ocran/host_config_helper.rb +11 -1
  18. data/lib/ocran/inno_setup_script_builder.rb +24 -14
  19. data/lib/ocran/launcher_batch_builder.rb +8 -11
  20. data/lib/ocran/npm_package.rb +106 -0
  21. data/lib/ocran/option.rb +465 -35
  22. data/lib/ocran/pe_imports.rb +96 -0
  23. data/lib/ocran/pe_resources.rb +226 -0
  24. data/lib/ocran/refine_pathname.rb +29 -32
  25. data/lib/ocran/roundhouse_builder.rb +316 -0
  26. data/lib/ocran/runner.rb +52 -11
  27. data/lib/ocran/runtime_sdk.rb +259 -0
  28. data/lib/ocran/spinel_builder.rb +183 -0
  29. data/lib/ocran/spinel_compatibility.rb +149 -0
  30. data/lib/ocran/static_runtime/boot.rb +443 -0
  31. data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
  32. data/lib/ocran/static_runtime_builder.rb +814 -0
  33. data/lib/ocran/stub_builder.rb +70 -25
  34. data/lib/ocran/version.rb +1 -1
  35. data/lib/ocran/wasm_builder.rb +570 -0
  36. data/lib/ocran/wasm_compatibility.rb +198 -0
  37. data/lib/ocran/windows_command_escaping.rb +21 -0
  38. data/lib/ocran/zip_payload_builder.rb +18 -9
  39. data/lib/ocran/zip_writer.rb +48 -9
  40. data/src/Makefile +18 -2
  41. data/src/Makefile.mswin +65 -0
  42. data/src/inst_dir.c +34 -6
  43. data/src/path_utils.c +144 -0
  44. data/src/script_info.c +5 -0
  45. data/src/stub.c +77 -19
  46. data/src/stub.rc +1 -1
  47. data/src/system_utils.c +276 -155
  48. data/src/system_utils.h +46 -3
  49. data/src/system_utils_posix.c +243 -148
  50. data/src/unpack.c +9 -0
  51. metadata +25 -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")