ocran 1.3.15 → 1.3.17

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.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ require "tempfile"
3
+ require_relative "file_path_set"
4
+ require_relative "windows_command_escaping"
5
+
6
+ module Ocran
7
+ class InnoSetupScriptBuilder
8
+ ISCC_CMD = "ISCC"
9
+ ISCC_SUCCESS = 0
10
+ ISCC_INVALID_PARAMS = 1
11
+ ISCC_COMPILATION_FAILED = 2
12
+
13
+ extend WindowsCommandEscaping
14
+
15
+ class << self
16
+ def compile(iss_filename, quiet: false)
17
+ unless system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
18
+ raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
19
+ end
20
+
21
+ cmd_line = [ISCC_CMD]
22
+ cmd_line << "/Q" if quiet
23
+ cmd_line << iss_filename
24
+ system(*cmd_line)
25
+
26
+ case $?&.exitstatus
27
+ when ISCC_SUCCESS
28
+ # ISCC reported success
29
+ when ISCC_INVALID_PARAMS
30
+ raise "ISCC reports invalid command line parameters"
31
+ when ISCC_COMPILATION_FAILED
32
+ raise "ISCC reports that compilation failed"
33
+ else
34
+ raise "ISCC failed to run"
35
+ end
36
+ end
37
+ end
38
+
39
+ include WindowsCommandEscaping
40
+
41
+ def initialize(inno_setup_script)
42
+ # ISSC generates the installer files relative to the directory of the
43
+ # ISS file. Therefore, it is necessary to create Tempfiles in the
44
+ # working directory.
45
+ @build_file = Tempfile.new("", Dir.pwd)
46
+ if inno_setup_script
47
+ IO.copy_stream(inno_setup_script, @build_file)
48
+ end
49
+ @dirs = FilePathSet.new
50
+ @files = FilePathSet.new
51
+ end
52
+
53
+ def build
54
+ @build_file.tap do |f|
55
+ if @dirs.any?
56
+ f.puts
57
+ f.puts "[Dirs]"
58
+ @dirs.each { |_source, target| f.puts build_dir_item(target) }
59
+ end
60
+
61
+ if @files.any?
62
+ f.puts
63
+ f.puts "[Files]"
64
+ @files.each { |source, target| f.puts build_file_item(source, target) }
65
+ end
66
+ end.close
67
+ path
68
+ end
69
+
70
+ def path
71
+ @build_file.to_path
72
+ end
73
+
74
+ def compile(verbose: false)
75
+ InnoSetupScriptBuilder.compile(path, quiet: !verbose)
76
+ end
77
+
78
+ def mkdir(target)
79
+ @dirs.add?("/", target)
80
+ end
81
+
82
+ def cp(source, target)
83
+ unless File.exist?(source)
84
+ raise "The file does not exist (#{source})"
85
+ end
86
+
87
+ @files.add?(source, target)
88
+ end
89
+
90
+ def build_dir_item(target)
91
+ name = File.join("{app}", target)
92
+ "Name: #{quote_and_escape(name)};"
93
+ end
94
+ private :build_dir_item
95
+
96
+ def build_file_item(source, target)
97
+ dest_dir = File.join("{app}", File.dirname(target))
98
+ s = [
99
+ "Source: #{quote_and_escape(source)};",
100
+ "DestDir: #{quote_and_escape(dest_dir)};"
101
+ ]
102
+ src_name = File.basename(source)
103
+ dest_name = File.basename(target)
104
+ if src_name != dest_name
105
+ s << "DestName: #{quote_and_escape(dest_name)};"
106
+ end
107
+ s.join(" ")
108
+ end
109
+ private :build_file_item
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ require "tempfile"
3
+ require_relative "windows_command_escaping"
4
+ require_relative "build_constants"
5
+
6
+ module Ocran
7
+ class LauncherBatchBuilder
8
+ include BuildConstants, WindowsCommandEscaping
9
+
10
+ # BATCH_FILE_DIR is a parameter expansion used in Windows batch files,
11
+ # representing the full path to the directory where the batch file resides.
12
+ # It allows for the use of pseudo-relative paths by referencing the
13
+ # batch file's own location without changing the working directory.
14
+ BATCH_FILE_DIR = "%~dp0"
15
+
16
+ # BATCH_FILE_PATH is a parameter expansion used in Windows batch files,
17
+ # representing the full path to the batch file itself, including the file name.
18
+ BATCH_FILE_PATH = "%~f0"
19
+
20
+ def initialize(chdir_before: nil, title: nil)
21
+ @build_file = Tempfile.new
22
+ @title = title
23
+ @chdir_before = chdir_before
24
+ @environments = {}
25
+ end
26
+
27
+ def build
28
+ @build_file.tap do |f|
29
+ f.puts "@echo off"
30
+ @environments.each { |name, val| f.puts build_set_command(name, val) }
31
+ f.puts build_set_command("OCRAN_EXECUTABLE", BATCH_FILE_PATH)
32
+ f.puts build_start_command(@title, @executable, @script, *@args, chdir_before: @chdir_before)
33
+ f
34
+ end.close
35
+ path
36
+ end
37
+
38
+ def path
39
+ @build_file.to_path
40
+ end
41
+
42
+ def export(name, value)
43
+ @environments[name] = value
44
+ end
45
+
46
+ def exec(executable, script, *args)
47
+ @executable, @script, @args = executable, script, args
48
+ end
49
+
50
+ def replace_inst_dir_placeholder(s)
51
+ s.to_s.gsub(/#{Regexp.escape(EXTRACT_ROOT.to_s)}[\/\\]/, BATCH_FILE_DIR)
52
+ end
53
+ private :replace_inst_dir_placeholder
54
+
55
+ def build_set_command(name, value)
56
+ "set \"#{name}=#{replace_inst_dir_placeholder(value)}\""
57
+ end
58
+ private :build_set_command
59
+
60
+ def build_start_command(title, executable, script, *args, chdir_before: nil)
61
+ cmd = ["start"]
62
+
63
+ # Title for Command Prompt window title bar
64
+ cmd << quote_and_escape(title)
65
+
66
+ # Use /d to set the startup directory for the process,
67
+ # which will be BATCH_FILE_DIR/SRCDIR. This path is where
68
+ # the script is located, establishing the working directory
69
+ # at process start.
70
+ if chdir_before
71
+ cmd << "/d #{quote_and_escape("#{BATCH_FILE_DIR}#{SRCDIR}")}"
72
+ end
73
+
74
+ cmd << quote_and_escape("#{BATCH_FILE_DIR}#{executable}")
75
+ cmd << quote_and_escape("#{BATCH_FILE_DIR}#{script}")
76
+ cmd += args.map { |arg| quote_and_escape(replace_inst_dir_placeholder(arg)) }
77
+
78
+ # Forward batch file arguments to the command with `%*`
79
+ cmd << "%*"
80
+
81
+ cmd.join(" ")
82
+ end
83
+ private :build_start_command
84
+ end
85
+ end
@@ -0,0 +1,61 @@
1
+ require "fiddle/import"
2
+ require "fiddle/types"
3
+
4
+ module Ocran
5
+ module LibraryDetector
6
+ # Windows API functions for handling files may return long paths,
7
+ # with a maximum character limit of 32,767.
8
+ # "\\?\" prefix(4 characters) + long path(32762 characters) + NULL = 32767 characters
9
+ # https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
10
+ MAX_PATH = 32767
11
+
12
+ # The byte size of the buffer given as an argument to the EnumProcessModules function.
13
+ # This buffer is used to store the handles of the loaded modules.
14
+ # If the buffer size is smaller than the number of loaded modules,
15
+ # it will automatically increase the buffer size and call the EnumProcessModules function again.
16
+ # Increasing the initial buffer size can reduce the number of iterations required.
17
+ # https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-modules-for-a-process
18
+ DEFAULT_HMODULE_BUFFER_SIZE = 1024
19
+
20
+ extend Fiddle::Importer
21
+ dlload "kernel32.dll", "psapi.dll"
22
+
23
+ include Fiddle::Win32Types
24
+ # https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
25
+ typealias "HMODULE", "HINSTANCE"
26
+ typealias "LPDWORD", "PDWORD"
27
+ typealias "LPWSTR", "char*"
28
+
29
+ extern "BOOL EnumProcessModules(HANDLE, HMODULE*, DWORD, LPDWORD)"
30
+ extern "DWORD GetModuleFileNameW(HMODULE, LPWSTR, DWORD)"
31
+ extern "HANDLE GetCurrentProcess()"
32
+ extern "DWORD GetLastError()"
33
+
34
+ class << self
35
+ def loaded_dlls
36
+ dword = "L" # A DWORD is a 32-bit unsigned integer.
37
+ bytes_needed = [0].pack(dword)
38
+ bytes = DEFAULT_HMODULE_BUFFER_SIZE
39
+ process_handle = GetCurrentProcess()
40
+ handles = while true
41
+ buffer = "\x00" * bytes
42
+ if EnumProcessModules(process_handle, buffer, buffer.bytesize, bytes_needed) == 0
43
+ raise "EnumProcessModules failed with error code #{GetLastError()}"
44
+ end
45
+ bytes = bytes_needed.unpack1(dword)
46
+ if bytes <= buffer.bytesize
47
+ break buffer.unpack("J#{bytes / Fiddle::SIZEOF_VOIDP}")
48
+ end
49
+ end
50
+ str = "\x00".encode("UTF-16LE") * MAX_PATH
51
+ handles.map do |handle|
52
+ length = GetModuleFileNameW(handle, str, str.bytesize)
53
+ if length == 0
54
+ raise "GetModuleFileNameW failed with error code #{GetLastError()}"
55
+ end
56
+ str[0, length].encode("UTF-8")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+
4
+ module Ocran
5
+ class Option
6
+ load File.expand_path("refine_pathname.rb", __dir__) unless defined? RefinePathname
7
+ using RefinePathname
8
+
9
+ def initialize
10
+ @options = {
11
+ :add_all_core? => false,
12
+ :add_all_encoding? => true,
13
+ :argv => [],
14
+ :auto_detect_dlls? => true,
15
+ :chdir_before? => false,
16
+ :enable_compression? => true,
17
+ :enable_debug_extract? => false,
18
+ :enable_debug_mode? => false,
19
+ :extra_dlls => [],
20
+ :force_console? => false,
21
+ :force_windows? => false,
22
+ :gem_options => [],
23
+ :gemfile => nil,
24
+ :icon_filename => nil,
25
+ :inno_setup_script => nil,
26
+ :load_autoload? => true,
27
+ :output_override => nil,
28
+ :quiet? => false,
29
+ :rubyopt => nil,
30
+ :run_script? => true,
31
+ :script => nil,
32
+ :source_files => [],
33
+ :verbose? => false,
34
+ :warning? => true,
35
+ }
36
+ end
37
+
38
+ def usage
39
+ <<EOF
40
+ ocran [options] script.rb
41
+
42
+ Ocran options:
43
+
44
+ --help Display this information.
45
+ --quiet Suppress output while building executable.
46
+ --verbose Show extra output while building executable.
47
+ --version Display version number and exit.
48
+
49
+ Packaging options:
50
+
51
+ --dll dllname Include additional DLLs from the Ruby bindir.
52
+ --add-all-core Add all core ruby libraries to the executable.
53
+ --gemfile <file> Add all gems and dependencies listed in a Bundler Gemfile.
54
+ --no-enc Exclude encoding support files
55
+
56
+ Gem content detection modes:
57
+
58
+ --gem-minimal[=gem1,..] Include only loaded scripts
59
+ --gem-guess=[gem1,...] Include loaded scripts & best guess (DEFAULT)
60
+ --gem-all[=gem1,..] Include all scripts & files
61
+ --gem-full[=gem1,..] Include EVERYTHING
62
+ --gem-spec[=gem1,..] Include files in gemspec (Does not work with Rubygems 1.7+)
63
+
64
+ minimal: loaded scripts
65
+ guess: loaded scripts and other files
66
+ all: loaded scripts, other scripts, other files (except extras)
67
+ full: Everything found in the gem directory
68
+
69
+ --[no-]gem-scripts[=..] Other script files than those loaded
70
+ --[no-]gem-files[=..] Other files (e.g. data files)
71
+ --[no-]gem-extras[=..] Extra files (README, etc.)
72
+
73
+ scripts: .rb/.rbw files
74
+ extras: C/C++ sources, object files, test, spec, README
75
+ files: all other files
76
+
77
+ Auto-detection options:
78
+
79
+ --no-dep-run Don't run script.rb to check for dependencies.
80
+ --no-autoload Don't load/include script.rb's autoloads.
81
+ --no-autodll Disable detection of runtime DLL dependencies.
82
+
83
+ Output options:
84
+
85
+ --output <file> Name the exe to generate. Defaults to ./<scriptname>.exe.
86
+ --no-lzma Disable LZMA compression of the executable.
87
+ --innosetup <file> Use given Inno Setup script (.iss) to create an installer.
88
+
89
+ Executable options:
90
+
91
+ --windows Force Windows application (rubyw.exe)
92
+ --console Force console application (ruby.exe)
93
+ --chdir-first When exe starts, change working directory to app dir.
94
+ --icon <ico> Replace icon with a custom one.
95
+ --rubyopt <str> Set the RUBYOPT environment variable when running the executable
96
+ --debug Executable will be verbose.
97
+ --debug-extract Executable will unpack to local dir and not delete after.
98
+ EOF
99
+ end
100
+
101
+ def parse(argv)
102
+ while (arg = argv.shift)
103
+ case arg
104
+ when /\A--(no-)?lzma\z/
105
+ @options[:enable_compression?] = !$1
106
+ when "--no-dep-run"
107
+ @options[:run_script?] = false
108
+ when "--add-all-core"
109
+ @options[:add_all_core?] = true
110
+ when "--output"
111
+ path = argv.shift
112
+ @options[:output_override] = Pathname.new(path).expand_path if path
113
+ when "--dll"
114
+ path = argv.shift
115
+ @options[:extra_dlls] << path if path
116
+ when "--quiet"
117
+ @options[:quiet?] = true
118
+ when "--verbose"
119
+ @options[:verbose?] = true
120
+ when "--windows"
121
+ @options[:force_windows?] = true
122
+ when "--console"
123
+ @options[:force_console?] = true
124
+ when "--no-autoload"
125
+ @options[:load_autoload?] = false
126
+ when "--chdir-first"
127
+ @options[:chdir_before?] = true
128
+ when "--icon"
129
+ path = argv.shift
130
+ raise "Icon file #{path} not found" unless path && File.exist?(path)
131
+ @options[:icon_filename] = Pathname.new(path).expand_path
132
+ when "--rubyopt"
133
+ @options[:rubyopt] = argv.shift
134
+ when "--gemfile"
135
+ path = argv.shift
136
+ raise "Gemfile #{path} not found" unless path && File.exist?(path)
137
+ @options[:gemfile] = Pathname.new(path).expand_path
138
+ when "--innosetup"
139
+ path = argv.shift
140
+ raise "Inno Script #{path} not found" unless path && File.exist?(path)
141
+ @options[:inno_setup_script] = Pathname.new(path).expand_path
142
+ when "--no-autodll"
143
+ @options[:auto_detect_dlls?] = false
144
+ when "--version"
145
+ require_relative "version"
146
+ puts "Ocran #{VERSION}"
147
+ raise SystemExit
148
+ when "--no-warnings"
149
+ @options[:warning?] = false
150
+ when "--debug"
151
+ @options[:enable_debug_mode?] = true
152
+ when "--debug-extract"
153
+ @options[:enable_debug_extract?] = true
154
+ when "--"
155
+ @options[:argv] = argv.dup
156
+ argv.clear
157
+ break
158
+ when /\A--(no-)?enc\z/
159
+ @options[:add_all_encoding?] = !$1
160
+ when /\A--(no-)?gem-(\w+)(?:=(.*))?$/
161
+ negate, group, list = $1, $2, $3
162
+ @options[:gem_options] << [negate, group.to_sym, list&.split(",")] if group
163
+ when "--help", /\A--./
164
+ puts usage
165
+ raise SystemExit
166
+ else
167
+ raise "#{arg} not found!" unless File.exist?(arg)
168
+
169
+ if File.directory?(arg)
170
+ raise "#{arg} is empty!" if Dir.empty?(arg)
171
+ # If a directory is passed, we want all files under that directory
172
+ @options[:source_files] += Pathname.new(arg).find.reject(&:directory?).map(&:expand_path)
173
+ else
174
+ @options[:source_files] << Pathname.new(arg).expand_path
175
+ end
176
+ end
177
+ end
178
+
179
+ raise "No script file specified" if source_files.empty?
180
+
181
+ @options[:script] = source_files.first
182
+
183
+ @options[:force_autoload?] = run_script? && load_autoload?
184
+
185
+ @options[:output_executable] =
186
+ if output_override
187
+ output_override
188
+ else
189
+ executable = script
190
+ # If debug mode is enabled, append "-debug" to the filename
191
+ executable = executable.append_to_filename("-debug") if enable_debug_mode?
192
+ # Build output files are created in the current directory
193
+ executable.basename.sub_ext(".exe").expand_path
194
+ end
195
+
196
+ @options[:use_inno_setup?] = !!inno_setup_script
197
+
198
+ @options[:verbose?] &&= !quiet?
199
+
200
+ @options[:windowed?] = (script.extname?(".rbw") || force_windows?) && !force_console?
201
+
202
+ if inno_setup_script
203
+ if enable_debug_extract?
204
+ raise "The --debug-extract option conflicts with use of Inno Setup"
205
+ end
206
+
207
+ if enable_compression?
208
+ raise "LZMA compression must be disabled (--no-lzma) when using Inno Setup"
209
+ end
210
+
211
+ unless chdir_before?
212
+ raise "Chdir-first mode must be enabled (--chdir-first) when using Inno Setup"
213
+ end
214
+ end
215
+ end
216
+
217
+ def add_all_core? = @options[__method__]
218
+
219
+ def add_all_encoding? = @options[__method__]
220
+
221
+ def argv = @options[__method__]
222
+
223
+ def auto_detect_dlls? = @options[__method__]
224
+
225
+ def chdir_before? = @options[__method__]
226
+
227
+ def enable_compression? = @options[__method__]
228
+
229
+ def enable_debug_extract? = @options[__method__]
230
+
231
+ def enable_debug_mode? = @options[__method__]
232
+
233
+ def extra_dlls = @options[__method__]
234
+
235
+ def force_autoload? = @options[__method__]
236
+
237
+ def force_console? = @options[__method__]
238
+
239
+ def force_windows? = @options[__method__]
240
+
241
+ def gem_options = @options[__method__]
242
+
243
+ def gemfile = @options[__method__]
244
+
245
+ def icon_filename = @options[__method__]
246
+
247
+ def inno_setup_script = @options[__method__]
248
+
249
+ def load_autoload? = @options[__method__]
250
+
251
+ def output_executable = @options[__method__]
252
+
253
+ def output_override = @options[__method__]
254
+
255
+ def quiet? = @options[__method__]
256
+
257
+ def rubyopt = @options[__method__]
258
+
259
+ def run_script? = @options[__method__]
260
+
261
+ def script = @options[__method__]
262
+
263
+ def source_files = @options[__method__]
264
+
265
+ def use_inno_setup? = @options[__method__]
266
+
267
+ def verbose? = @options[__method__]
268
+
269
+ def warning? = @options[__method__]
270
+
271
+ def windowed? = @options[__method__]
272
+ end
273
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+
4
+ module Ocran
5
+ # The Pathname class in Ruby is modified to handle mixed path separators and
6
+ # to be case-insensitive.
7
+ module RefinePathname
8
+ refine Pathname do
9
+ def normalize_file_separator(s)
10
+ if File::ALT_SEPARATOR
11
+ s.tr(File::ALT_SEPARATOR, File::SEPARATOR)
12
+ else
13
+ s
14
+ end
15
+ end
16
+ private :normalize_file_separator
17
+
18
+ # Compares two paths for equality based on the case sensitivity of the
19
+ # Ruby execution environment's file system.
20
+ # If the file system is case-insensitive, it performs a case-insensitive
21
+ # comparison. Otherwise, it performs a case-sensitive comparison.
22
+ def pathequal(a, b)
23
+ if File::FNM_SYSCASE.nonzero?
24
+ a.casecmp(b) == 0
25
+ else
26
+ a == b
27
+ end
28
+ end
29
+ private :pathequal
30
+
31
+ def to_posix
32
+ normalize_file_separator(to_s)
33
+ end
34
+
35
+ # Checks if two Pathname objects are equal, considering the file system's
36
+ # case sensitivity and path separators. Returns false if the other object is not
37
+ # an Pathname.
38
+ # This method enables the use of the `uniq` method on arrays of Pathname objects.
39
+ def eql?(other)
40
+ return false unless other.is_a?(Pathname)
41
+
42
+ a = normalize_file_separator(to_s)
43
+ b = normalize_file_separator(other.to_s)
44
+ pathequal(a, b)
45
+ end
46
+
47
+ alias == eql?
48
+ alias === eql?
49
+
50
+ # Calculates a normalized hash value for a pathname to ensure consistent
51
+ # hashing across different environments, particularly in Windows.
52
+ # This method first normalizes the path by:
53
+ # 1. Converting the file separator from the platform-specific separator
54
+ # to the common POSIX separator ('/') if necessary.
55
+ # 2. Converting the path to lowercase if the filesystem is case-insensitive.
56
+ # The normalized path string is then hashed, providing a stable hash value
57
+ # that is consistent with the behavior of eql? method, thus maintaining
58
+ # the integrity of hash-based data structures like Hash or Set.
59
+ #
60
+ # @return [Integer] A hash integer based on the normalized path.
61
+ def hash
62
+ path = if File::FNM_SYSCASE.nonzero?
63
+ to_s.downcase
64
+ else
65
+ to_s
66
+ end
67
+ normalize_file_separator(path).hash
68
+ end
69
+
70
+ # Checks if the current path is a sub path of the specified base_directory.
71
+ # Both paths must be either absolute paths or relative paths; otherwise, this
72
+ # method returns false.
73
+ def subpath?(base_directory)
74
+ s = relative_path_from(base_directory).each_filename.first
75
+ s != '.' && s != ".."
76
+ rescue ArgumentError
77
+ false
78
+ end
79
+
80
+ # Appends the given suffix to the filename, preserving the file extension.
81
+ # If the filename has an extension, the suffix is inserted before the extension.
82
+ # If the filename does not have an extension, the suffix is appended to the end.
83
+ # This method handles both directory and file paths correctly.
84
+ #
85
+ # Examples:
86
+ # pathname = Pathname("path.to/foo.tar.gz")
87
+ # pathname.append_to_filename("_bar") # => #<Pathname:path.to/foo_bar.tar.gz>
88
+ #
89
+ # pathname = Pathname("path.to/foo")
90
+ # pathname.append_to_filename("_bar") # => #<Pathname:path.to/foo_bar>
91
+ #
92
+ def append_to_filename(suffix)
93
+ dirname + basename.sub(/(\.?[^.]+)?(\..*)?\z/, "\\1#{suffix}\\2")
94
+ end
95
+
96
+ # Checks if the file's extension matches the expected extension.
97
+ # The comparison is case-insensitive.
98
+ # Example usage: ocran_pathname.extname?(".exe")
99
+ def extname?(expected_ext)
100
+ extname.casecmp(expected_ext) == 0
101
+ end
102
+ end
103
+ end
104
+ end