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,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
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+ require "tempfile"
3
+ require_relative "file_path_set"
4
+
5
+ module Ocran
6
+ # Utility class that produces the actual executable. Opcodes
7
+ # (create_file, mkdir etc) are added by invoking methods on an
8
+ # instance of OcranBuilder.
9
+ class StubBuilder
10
+ Signature = [0x41, 0xb6, 0xba, 0x4e].freeze
11
+ OP_END = 0
12
+ OP_CREATE_DIRECTORY = 1
13
+ OP_CREATE_FILE = 2
14
+ OP_SETENV = 3
15
+ OP_SET_SCRIPT = 4
16
+
17
+ DEBUG_MODE = 0x01
18
+ EXTRACT_TO_EXE_DIR = 0x02
19
+ AUTO_CLEAN_INST_DIR = 0x04
20
+ CHDIR_BEFORE_SCRIPT = 0x08
21
+ DATA_COMPRESSED = 0x10
22
+
23
+ base_dir = File.expand_path("../../share/ocran", File.dirname(__FILE__))
24
+ STUB_PATH = File.expand_path("stub.exe", base_dir)
25
+ STUBW_PATH = File.expand_path("stubw.exe", base_dir)
26
+ LZMA_PATH = File.expand_path("lzma.exe", base_dir)
27
+ EDICON_PATH = File.expand_path("edicon.exe", base_dir)
28
+
29
+ attr_reader :data_size
30
+
31
+ # chdir_before:
32
+ # When set to true, the working directory is changed to the application's
33
+ # deployment location at runtime.
34
+ #
35
+ # debug_mode:
36
+ # When the debug_mode option is set to true, the stub will output debug information
37
+ # when the exe file is executed. Debug mode can also be enabled within the directive
38
+ # code using the enable_debug_mode method. This option is provided to transition to
39
+ # debug mode from the initialization point of the stub.
40
+ #
41
+ # debug_extract:
42
+ # When set to true, the runtime file is extracted to the directory where the executable resides,
43
+ # and the extracted files remain even after the application exits.
44
+ # When set to false, the runtime file is extracted to the system's temporary directory,
45
+ # and the extracted files are deleted after the application exits.
46
+ #
47
+ # gui_mode:
48
+ # When set to true, the stub does not display a console window at startup. Errors are shown in a dialog window.
49
+ # When set to false, the stub reports errors through the console window.
50
+ #
51
+ # icon_path:
52
+ # Specifies the path to the icon file to be embedded in the stub's resources.
53
+ #
54
+ def initialize(path, chdir_before: nil, debug_extract: nil, debug_mode: nil,
55
+ enable_compression: nil, gui_mode: nil, icon_path: nil)
56
+ @dirs = FilePathSet.new
57
+ @files = FilePathSet.new
58
+ @data_size = 0
59
+
60
+ if icon_path && !File.exist?(icon_path)
61
+ raise "Icon file #{icon_path} not found"
62
+ end
63
+
64
+ stub = Tempfile.new("", File.dirname(path))
65
+ IO.copy_stream(gui_mode ? STUBW_PATH : STUB_PATH, stub)
66
+ stub.close
67
+
68
+ if icon_path
69
+ system(EDICON_PATH, stub.path, icon_path.to_s, exception: true)
70
+ end
71
+
72
+ File.open(stub, "ab") do |of|
73
+ @of = of
74
+ @opcode_offset = @of.size
75
+
76
+ write_header(debug_mode, debug_extract, chdir_before, enable_compression)
77
+
78
+ b = proc {
79
+ yield(self)
80
+ write_opcode(OP_END)
81
+ }
82
+
83
+ if enable_compression
84
+ compress(&b)
85
+ else
86
+ b.yield
87
+ end
88
+
89
+ write_footer
90
+ end
91
+
92
+ File.rename(stub, path)
93
+ end
94
+
95
+ def mkdir(target)
96
+ return unless @dirs.add?("/", target)
97
+
98
+ write_opcode(OP_CREATE_DIRECTORY)
99
+ write_path(target)
100
+ end
101
+
102
+ def cp(source, target)
103
+ unless File.exist?(source)
104
+ raise "The file does not exist (#{source})"
105
+ end
106
+
107
+ return unless @files.add?(source, target)
108
+
109
+ write_opcode(OP_CREATE_FILE)
110
+ write_path(target)
111
+ write_file(source)
112
+ end
113
+
114
+ # Specifies the final application script to be launched, which can be called
115
+ # from any position in the data stream. It cannot be specified more than once.
116
+ #
117
+ # You can omit setting OP_SET_SCRIPT without issues, in which case
118
+ # the stub terminates without launching anything after performing other
119
+ # runtime operations.
120
+ def exec(image, script, *argv)
121
+ if @script_set
122
+ raise "Script is already set"
123
+ end
124
+ @script_set = true
125
+
126
+ write_opcode(OP_SET_SCRIPT)
127
+ write_string_array(convert_to_native(image), convert_to_native(script), *argv)
128
+ end
129
+
130
+ def export(name, value)
131
+ write_opcode(OP_SETENV)
132
+ write_string(name.to_s)
133
+ write_string(value.to_s)
134
+ end
135
+
136
+ def compress
137
+ IO.popen([LZMA_PATH, "e", "-si", "-so"], "r+b") do |lzma|
138
+ _of, @of = @of, lzma
139
+ Thread.new { yield(self); lzma.close_write }
140
+ IO.copy_stream(lzma, _of)
141
+ @of = _of
142
+ end
143
+
144
+ # Calculate the position to write the LZMA decompressed size (64-bit unsigned integer)
145
+ # @opcode_offset: start position of the data section
146
+ # 1: size of the header byte
147
+ # 5: size of the LZMA header in bytes
148
+ File.binwrite(@of.path, [@data_size].pack("Q<"), @opcode_offset + 1 + 5)
149
+ end
150
+ private :compress
151
+
152
+ def write_header(debug_mode, debug_extract, chdir_before, compressed)
153
+ next_to_exe, delete_after = debug_extract, !debug_extract
154
+ @of << [0 |
155
+ (debug_mode ? DEBUG_MODE : 0) |
156
+ (next_to_exe ? EXTRACT_TO_EXE_DIR : 0) |
157
+ (delete_after ? AUTO_CLEAN_INST_DIR : 0) |
158
+ (chdir_before ? CHDIR_BEFORE_SCRIPT : 0) |
159
+ (compressed ? DATA_COMPRESSED : 0)
160
+ ].pack("C")
161
+ end
162
+ private :write_header
163
+
164
+ def write_opcode(op)
165
+ @of << [op].pack("C")
166
+ @data_size += 1
167
+ end
168
+ private :write_opcode
169
+
170
+ def write_size(i)
171
+ if i > 0xFFFF_FFFF
172
+ raise ArgumentError, "Size #{i} is too large: must be 32-bit unsigned integer (0 to 4294967295)"
173
+ end
174
+
175
+ @of << [i].pack("V")
176
+ @data_size += 4
177
+ end
178
+ private :write_size
179
+
180
+ def write_string(str)
181
+ len = str.bytesize + 1 # +1 to account for the null terminator
182
+
183
+ if len > 0xFFFF
184
+ raise ArgumentError, "String length #{len} is too large: must be less than or equal to 65535 bytes including null terminator"
185
+ end
186
+
187
+ @of << [len, str].pack("vZ*")
188
+ @data_size += 2 + len
189
+ end
190
+ private :write_string
191
+
192
+ def write_string_array(*str_array)
193
+ ary = str_array.map(&:to_s)
194
+ size = ary.sum(0) { |s| s.bytesize + 1 }
195
+ write_size(size)
196
+ ary.each_slice(1) { |a| @of << a.pack("Z*") }
197
+ @data_size += size
198
+ end
199
+ private :write_string_array
200
+
201
+ def write_file(src)
202
+ size = File.size(src)
203
+ write_size(size)
204
+ IO.copy_stream(src, @of)
205
+ @data_size += size
206
+ end
207
+ private :write_file
208
+
209
+ def write_path(path)
210
+ write_string(convert_to_native(path))
211
+ end
212
+ private :write_path
213
+
214
+ def write_footer
215
+ @of << ([@opcode_offset] + Signature).pack("VC*")
216
+ end
217
+ private :write_footer
218
+
219
+ def convert_to_native(path)
220
+ path.to_s.tr(File::SEPARATOR, "\\")
221
+ end
222
+ private :convert_to_native
223
+ end
224
+ end
data/lib/ocran/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ocran
4
- VERSION = "1.3.15"
4
+ VERSION = "1.3.17"
5
5
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ module WindowsCommandEscaping
5
+ module_function
6
+
7
+ def escape_double_quotes(s)
8
+ s.to_s.gsub('"', '""')
9
+ end
10
+
11
+ def quote_and_escape(s)
12
+ "\"#{escape_double_quotes(s)}\""
13
+ end
14
+ end
15
+ end
data/lib/ocran.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ocran/version"
4
-
5
3
  module Ocran
4
+ autoload :VERSION, "ocran/version"
5
+
6
+ singleton_class.attr_accessor :option
6
7
  end
Binary file
data/share/ocran/stub.exe CHANGED
Binary file
Binary file
metadata CHANGED
@@ -1,21 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocran
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.15
4
+ version: 1.3.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andi Idogawa
8
8
  - Lars Christensen
9
9
  autorequire:
10
- bindir: bin
10
+ bindir: exe
11
11
  cert_chain: []
12
- date: 2023-11-30 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2025-05-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fiddle
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.0'
14
28
  description: "OCRAN (One-Click Ruby Application Next) builds Windows executables from
15
29
  Ruby source code. \n The executable is a self-extracting, self-running executable
16
30
  that contains the Ruby interpreter, your source code and any additionally needed
17
31
  ruby libraries or DLL.\n \n This is a fork of OCRA that is compatible with ruby
18
- version after 2.6. \n Migration guide: make sure to write ocran instead of ocra
32
+ version after 3.0.\n Migration guide: make sure to write ocran instead of ocra
19
33
  in your code. For instance: OCRAN_EXECUTABLE\n\n usage: \n ocra helloworld.rb\n
20
34
  \ helloworld.exe\n\n See readme at https://github.com/largo/ocran\n Report problems
21
35
  in the github issues. Contributions welcome.\n This gem contains executables. We
@@ -27,11 +41,31 @@ executables:
27
41
  extensions: []
28
42
  extra_rdoc_files: []
29
43
  files:
30
- - bin/ocran
44
+ - CHANGELOG.txt
45
+ - LICENSE.txt
46
+ - README.md
47
+ - exe/ocran
31
48
  - lib/ocran.rb
49
+ - lib/ocran/build_constants.rb
50
+ - lib/ocran/build_facade.rb
51
+ - lib/ocran/build_helper.rb
52
+ - lib/ocran/command_output.rb
53
+ - lib/ocran/direction.rb
54
+ - lib/ocran/empty_source
55
+ - lib/ocran/file_path_set.rb
56
+ - lib/ocran/gem_spec_queryable.rb
57
+ - lib/ocran/host_config_helper.rb
58
+ - lib/ocran/inno_setup_script_builder.rb
59
+ - lib/ocran/launcher_batch_builder.rb
60
+ - lib/ocran/library_detector.rb
61
+ - lib/ocran/option.rb
62
+ - lib/ocran/refine_pathname.rb
63
+ - lib/ocran/runner.rb
64
+ - lib/ocran/runtime_environment.rb
65
+ - lib/ocran/stub_builder.rb
32
66
  - lib/ocran/version.rb
67
+ - lib/ocran/windows_command_escaping.rb
33
68
  - share/ocran/edicon.exe
34
- - share/ocran/empty-msys-2.0.dll
35
69
  - share/ocran/lzma.exe
36
70
  - share/ocran/stub.exe
37
71
  - share/ocran/stubw.exe
@@ -50,14 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
84
  requirements:
51
85
  - - ">="
52
86
  - !ruby/object:Gem::Version
53
- version: 2.6.0
87
+ version: 3.0.0
54
88
  required_rubygems_version: !ruby/object:Gem::Requirement
55
89
  requirements:
56
90
  - - ">="
57
91
  - !ruby/object:Gem::Version
58
92
  version: '0'
59
93
  requirements: []
60
- rubygems_version: 3.4.10
94
+ rubygems_version: 3.5.3
61
95
  signing_key:
62
96
  specification_version: 4
63
97
  summary: OCRAN (One-Click Ruby Application Next) builds Windows executables from Ruby