ocran 1.3.15 → 1.3.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ load File.expand_path("../ocran.rb", __dir__)
3
+
4
+ module Ocran
5
+ class Runner
6
+ load File.expand_path("command_output.rb", __dir__)
7
+ include CommandOutput
8
+
9
+ def fatal_error(statement)
10
+ error statement
11
+ exit false
12
+ end
13
+
14
+ def initialize
15
+ load File.expand_path("runtime_environment.rb", __dir__)
16
+ @pre_env = RuntimeEnvironment.save
17
+
18
+ load File.expand_path("option.rb", __dir__)
19
+ @option = Option.new.tap do |opt|
20
+ opt.parse(ARGV)
21
+ rescue RuntimeError => e
22
+ # Capture RuntimeError during parsing and display an appropriate
23
+ # error message to the user. This error usually occurs from invalid
24
+ # option arguments.
25
+ fatal_error e.message
26
+ else
27
+ # Update ARGV with the parsed command line arguments to pass to
28
+ # the user's script. This ensures the script executes based on
29
+ # the user-specified arguments.
30
+ ARGV.replace(opt.argv)
31
+ end
32
+
33
+ Ocran.option = @option
34
+
35
+ @ignore_modules = ObjectSpace.each_object(Module).to_a
36
+ end
37
+
38
+ def run
39
+ at_exit do
40
+ if $!.nil? or $!.kind_of?(SystemExit)
41
+ build
42
+ exit
43
+ end
44
+ end
45
+
46
+ exit unless @option.run_script?
47
+ say "Loading script to check dependencies"
48
+ $PROGRAM_NAME = @option.script.to_s
49
+ end
50
+
51
+ # Force loading autoloaded constants. Searches through all modules
52
+ # (and hence classes), and checks their constants for autoloaded
53
+ # ones, then attempts to load them.
54
+ def attempt_load_autoload(ignore_modules = [])
55
+ checked_modules = ignore_modules.inject({}) { |h, mod| h[mod] = true; h }
56
+ while ObjectSpace.each_object(Module).count { |mod|
57
+ next if checked_modules.include?(mod)
58
+ mod.constants.each do |const|
59
+ next unless mod.autoload?(const)
60
+ say "Attempting to trigger autoload of #{mod}::#{const}"
61
+ begin
62
+ mod.const_get(const)
63
+ rescue ScriptError, StandardError => e
64
+ # Some autoload constants may throw exceptions beyond the expected
65
+ # errors. This includes issues dependent on the system or execution
66
+ # environment, so it is preferable to ignore exceptions other than
67
+ # critical errors.
68
+ warning "#{mod}::#{const} loading failed: #{e.message}"
69
+ end
70
+ end
71
+ checked_modules[mod] = true
72
+ }.nonzero?
73
+ # Loops until all constants have been checked.
74
+ end
75
+ end
76
+
77
+ def build
78
+ # If the script was run and autoload is enabled, attempt to autoload libraries.
79
+ if @option.force_autoload?
80
+ attempt_load_autoload(@ignore_modules)
81
+ end
82
+
83
+ @post_env = RuntimeEnvironment.save
84
+ # NOTE: From this point, $LOADED_FEATURES has been captured, so it is now
85
+ # safe to call require_relative.
86
+
87
+ ENV.replace(@pre_env.env)
88
+
89
+ # It might be useful to reset the current directory to the point where the
90
+ # command was launched, especially when implementing the builder object.
91
+ Dir.chdir(@pre_env.pwd)
92
+
93
+ require_relative "direction"
94
+ direction = Direction.new(@post_env, @pre_env, @option)
95
+
96
+ if @option.use_inno_setup?
97
+ direction.build_inno_setup_installer
98
+ else
99
+ direction.build_stab_exe
100
+ end
101
+ rescue RuntimeError => e
102
+ fatal_error e.message
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+
4
+ module Ocran
5
+ load File.expand_path("refine_pathname.rb", __dir__) unless defined? RefinePathname
6
+ using RefinePathname
7
+
8
+ class RuntimeEnvironment
9
+ class << self
10
+ alias save new
11
+ end
12
+
13
+ attr_reader :env, :load_path, :loaded_features, :pwd
14
+
15
+ def initialize
16
+ @env = ENV.to_hash.freeze
17
+ @load_path = $LOAD_PATH.dup.freeze
18
+ @loaded_features = $LOADED_FEATURES.dup.freeze
19
+ @pwd = Dir.pwd.freeze
20
+ end
21
+
22
+ # Expands the given path using the working directory stored in this
23
+ # instance as the base. This method resolves relative paths to
24
+ # absolute paths, ensuring they are fully qualified based on the
25
+ # working directory stored within this instance.
26
+ def expand_path(path)
27
+ File.expand_path(path, @pwd)
28
+ end
29
+
30
+ def find_load_path(path)
31
+ path = Pathname.new(path) unless path.is_a?(Pathname)
32
+
33
+ if path.absolute?
34
+ # For an absolute path feature, find the load path that contains the feature
35
+ # and determine the longest matching path (most specific path).
36
+ @load_path.select { |load_path| path.subpath?(expand_path(load_path)) }
37
+ .max_by { |load_path| expand_path(load_path).length }
38
+ else
39
+ # For a relative path feature, find the load path where the expanded feature exists
40
+ # and select the longest load path (most specific path).
41
+ @load_path.select { |load_path| path.expand_path(load_path).exist? }
42
+ .max_by { |load_path| load_path.length }
43
+ end
44
+ end
45
+ end
46
+ end