ocran 1.4.2 → 1.4.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +26 -0
- data/README.md +119 -24
- data/lib/ocran/build_facade.rb +2 -0
- data/lib/ocran/dir_builder.rb +3 -0
- data/lib/ocran/direction.rb +245 -23
- data/lib/ocran/ed_icon.rb +157 -0
- data/lib/ocran/gem_spec_queryable.rb +24 -3
- data/lib/ocran/inno_setup_script_builder.rb +15 -1
- data/lib/ocran/launcher_event_recorder.rb +29 -0
- data/lib/ocran/option.rb +7 -0
- data/lib/ocran/runner.rb +4 -2
- data/lib/ocran/stub_builder.rb +21 -6
- data/lib/ocran/version.rb +1 -1
- data/src/Makefile +2 -4
- data/src/inst_dir.c +44 -2
- data/src/inst_dir.h +14 -0
- data/src/stub.c +19 -8
- data/src/system_utils.c +26 -0
- data/src/system_utils.h +14 -0
- data/src/system_utils_posix.c +6 -0
- data/src/unpack.c +4 -0
- data/src/unpack.h +9 -0
- metadata +12 -25
- data/src/edicon.c +0 -161
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "fiddle/import"
|
|
3
|
+
require "fiddle/types"
|
|
4
|
+
|
|
5
|
+
module Ocran
|
|
6
|
+
# Changes the Icon in a PE executable.
|
|
7
|
+
module EdIcon
|
|
8
|
+
extend Fiddle::Importer
|
|
9
|
+
dlload "kernel32.dll"
|
|
10
|
+
|
|
11
|
+
include Fiddle::Win32Types
|
|
12
|
+
typealias "LPVOID", "void*"
|
|
13
|
+
typealias "LPCWSTR", "char*"
|
|
14
|
+
|
|
15
|
+
module Successive
|
|
16
|
+
include Enumerable
|
|
17
|
+
|
|
18
|
+
def each
|
|
19
|
+
return to_enum(__method__) unless block_given?
|
|
20
|
+
|
|
21
|
+
entry = self
|
|
22
|
+
while true
|
|
23
|
+
yield(entry)
|
|
24
|
+
entry = self.class.new(tail)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tail
|
|
29
|
+
to_ptr + self.class.size
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Icon file header
|
|
34
|
+
IconHeader = struct(
|
|
35
|
+
[
|
|
36
|
+
"WORD Reserved",
|
|
37
|
+
"WORD ResourceType",
|
|
38
|
+
"WORD ImageCount"
|
|
39
|
+
]
|
|
40
|
+
).include(Successive)
|
|
41
|
+
|
|
42
|
+
icon_info = [
|
|
43
|
+
"BYTE Width",
|
|
44
|
+
"BYTE Height",
|
|
45
|
+
"BYTE Colors",
|
|
46
|
+
"BYTE Reserved",
|
|
47
|
+
"WORD Planes",
|
|
48
|
+
"WORD BitsPerPixel",
|
|
49
|
+
"DWORD ImageSize"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Icon File directory entry structure
|
|
53
|
+
IconDirectoryEntry = struct(icon_info + ["DWORD ImageOffset"]).include(Successive)
|
|
54
|
+
|
|
55
|
+
# Group Icon Resource directory entry structure
|
|
56
|
+
IconDirResEntry = struct(icon_info + ["WORD ResourceID"]).include(Successive)
|
|
57
|
+
|
|
58
|
+
class IconFile < IconHeader
|
|
59
|
+
def initialize(icon_filename)
|
|
60
|
+
@data = File.binread(icon_filename)
|
|
61
|
+
super(Fiddle::Pointer.to_ptr(@data))
|
|
62
|
+
|
|
63
|
+
entries_end = IconHeader.size + self.ImageCount * IconDirectoryEntry.size
|
|
64
|
+
if entries_end > @data.bytesize
|
|
65
|
+
raise "Icon file too small for declared ImageCount"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
entries.each_with_index do |entry, i|
|
|
69
|
+
if entry.ImageOffset + entry.ImageSize > @data.bytesize
|
|
70
|
+
raise "Icon entry #{i} exceeds file bounds"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def entries
|
|
76
|
+
IconDirectoryEntry.new(self.tail).take(self.ImageCount)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class GroupIcon < IconHeader
|
|
81
|
+
attr_reader :size
|
|
82
|
+
|
|
83
|
+
def initialize(image_count, resource_type)
|
|
84
|
+
@size = IconHeader.size + image_count * IconDirResEntry.size
|
|
85
|
+
super(Fiddle.malloc(@size), Fiddle::RUBY_FREE)
|
|
86
|
+
self.Reserved = 0
|
|
87
|
+
self.ResourceType = resource_type
|
|
88
|
+
self.ImageCount = image_count
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def entries
|
|
92
|
+
IconDirResEntry.new(self.tail).take(self.ImageCount)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
MAKEINTRESOURCE = -> (i) { Fiddle::Pointer.new(i) }
|
|
97
|
+
RT_ICON = MAKEINTRESOURCE.(3)
|
|
98
|
+
RT_GROUP_ICON = MAKEINTRESOURCE.(RT_ICON.to_i + 11)
|
|
99
|
+
|
|
100
|
+
MAKELANGID = -> (p, s) { s << 10 | p }
|
|
101
|
+
LANG_NEUTRAL = 0x00
|
|
102
|
+
SUBLANG_DEFAULT = 0x01
|
|
103
|
+
LANGID = MAKELANGID.(LANG_NEUTRAL, SUBLANG_DEFAULT)
|
|
104
|
+
|
|
105
|
+
extern "DWORD GetLastError()"
|
|
106
|
+
extern "HANDLE BeginUpdateResourceW(LPCWSTR, BOOL)"
|
|
107
|
+
extern "BOOL EndUpdateResourceW(HANDLE, BOOL)"
|
|
108
|
+
extern "BOOL UpdateResourceW(HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD)"
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
def update_icon(executable_filename, icon_filename)
|
|
112
|
+
update_resource(executable_filename) do |handle|
|
|
113
|
+
icon_file = IconFile.new(icon_filename)
|
|
114
|
+
icon_entries = icon_file.entries
|
|
115
|
+
|
|
116
|
+
# Create the RT_ICON resources
|
|
117
|
+
icon_entries.each_with_index do |entry, i|
|
|
118
|
+
if UpdateResourceW(handle, RT_ICON, 101 + i, LANGID, icon_file.to_i + entry.ImageOffset, entry.ImageSize) == 0
|
|
119
|
+
raise "failed to UpdateResource(#{GetLastError()})"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create the RT_GROUP_ICON structure
|
|
124
|
+
group_icon = GroupIcon.new(icon_file.ImageCount, icon_file.ResourceType)
|
|
125
|
+
group_icon.entries.zip(icon_entries).each_with_index do |(res, icon), i|
|
|
126
|
+
res.Width = icon.Width
|
|
127
|
+
res.Height = icon.Height
|
|
128
|
+
res.Colors = icon.Colors
|
|
129
|
+
res.Reserved = icon.Reserved
|
|
130
|
+
res.Planes = icon.Planes
|
|
131
|
+
res.BitsPerPixel = icon.BitsPerPixel
|
|
132
|
+
res.ImageSize = icon.ImageSize
|
|
133
|
+
res.ResourceID = 101 + i
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Save the RT_GROUP_ICON resource
|
|
137
|
+
if UpdateResourceW(handle, RT_GROUP_ICON, 100, LANGID, group_icon, group_icon.size) == 0
|
|
138
|
+
raise "Failed to create group icon(#{GetLastError()})"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def update_resource(executable_filename)
|
|
144
|
+
handle = BeginUpdateResourceW(executable_filename.encode("UTF-16LE"), 0)
|
|
145
|
+
if handle == Fiddle::NULL
|
|
146
|
+
raise "Failed to BeginUpdateResourceW(#{GetLastError()})"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
yield(handle)
|
|
150
|
+
|
|
151
|
+
if EndUpdateResourceW(handle, 0) == 0
|
|
152
|
+
raise "Failed to EndUpdateResourceW(#{GetLastError()})"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -131,8 +131,11 @@ module Ocran
|
|
|
131
131
|
|
|
132
132
|
def gem_root = Pathname(gem_dir)
|
|
133
133
|
|
|
134
|
-
# Find the selected files
|
|
135
|
-
|
|
134
|
+
# Find the selected files. Default gems (e.g. fiddle, singleton on
|
|
135
|
+
# Homebrew or distro-packaged Ruby) may have a gemspec without a
|
|
136
|
+
# materialized gem directory - their files live in Ruby's stdlib and
|
|
137
|
+
# are packed via the load path instead.
|
|
138
|
+
def gem_root_files = gem_root.directory? ? gem_root.find.select(&:file?) : []
|
|
136
139
|
|
|
137
140
|
def script_files
|
|
138
141
|
gem_root_files.select { |path| path.extname =~ GEM_SCRIPT_RE }
|
|
@@ -154,7 +157,25 @@ module Ocran
|
|
|
154
157
|
when :spec
|
|
155
158
|
files.map { |file| Pathname(file) }
|
|
156
159
|
when :loaded
|
|
157
|
-
|
|
160
|
+
# Some distros expose gem files under additional paths via symlinks
|
|
161
|
+
# (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the psych gem
|
|
162
|
+
# directory) and $LOADED_FEATURES records the symlink path. Resolve
|
|
163
|
+
# each feature to its realpath so such files count as gem files and
|
|
164
|
+
# are packed inside the gem directory. Packing them at the symlink
|
|
165
|
+
# location instead would break require_relative, which resolves
|
|
166
|
+
# against the realpath on the build host but not in the packed app.
|
|
167
|
+
features_from_gems.filter_map do |feature|
|
|
168
|
+
if feature.subpath?(gem_dir)
|
|
169
|
+
feature
|
|
170
|
+
else
|
|
171
|
+
real = begin
|
|
172
|
+
feature.realpath
|
|
173
|
+
rescue SystemCallError
|
|
174
|
+
next
|
|
175
|
+
end
|
|
176
|
+
real if real.subpath?(gem_dir)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
158
179
|
when :files
|
|
159
180
|
resource_files
|
|
160
181
|
when :extras
|
|
@@ -14,7 +14,14 @@ module Ocran
|
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
16
|
def compile(iss_filename, quiet: false)
|
|
17
|
-
|
|
17
|
+
# "where" and ">NUL" only exist on Windows; use "command -v" on POSIX
|
|
18
|
+
# (e.g. when testing the pipeline with a fake ISCC on Linux/macOS).
|
|
19
|
+
iscc_found = if Gem.win_platform?
|
|
20
|
+
true # ISCC is invoked directly; failure is reported below
|
|
21
|
+
else
|
|
22
|
+
system("command -v #{quote_and_escape(ISCC_CMD)} > /dev/null 2>&1")
|
|
23
|
+
end
|
|
24
|
+
unless iscc_found
|
|
18
25
|
raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
|
|
19
26
|
end
|
|
20
27
|
|
|
@@ -79,6 +86,13 @@ module Ocran
|
|
|
79
86
|
@dirs.add?("/", target)
|
|
80
87
|
end
|
|
81
88
|
|
|
89
|
+
# Symbolic links cannot be expressed in an Inno Setup script. They only
|
|
90
|
+
# occur when building from POSIX hosts (e.g. libruby.so links); Windows
|
|
91
|
+
# installations do not need them, so they are skipped.
|
|
92
|
+
def symlink(_target, _link_name)
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
82
96
|
def cp(source, target)
|
|
83
97
|
unless File.exist?(source)
|
|
84
98
|
raise "The file does not exist (#{source})"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ocran
|
|
4
|
+
# Records launcher-directed build events (environment exports and the
|
|
5
|
+
# application exec command) while forwarding them to another launcher
|
|
6
|
+
# builder. The recorded events can later be replayed into a further
|
|
7
|
+
# builder — e.g. to produce a wrapper stub executable alongside the
|
|
8
|
+
# launcher batch file in Inno Setup builds.
|
|
9
|
+
class LauncherEventRecorder
|
|
10
|
+
def initialize(launcher)
|
|
11
|
+
@launcher = launcher
|
|
12
|
+
@events = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def export(name, value)
|
|
16
|
+
@events << [:export, name, value]
|
|
17
|
+
@launcher.export(name, value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exec(image, script, *argv)
|
|
21
|
+
@events << [:exec, image, script, *argv]
|
|
22
|
+
@launcher.exec(image, script, *argv)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replay(builder)
|
|
26
|
+
@events.each { |event, *args| builder.public_send(event, *args) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/ocran/option.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Ocran
|
|
|
37
37
|
:source_files => [],
|
|
38
38
|
:verbose? => false,
|
|
39
39
|
:warning? => true,
|
|
40
|
+
:wrapper_exe? => true,
|
|
40
41
|
}
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -90,6 +91,8 @@ Output options:
|
|
|
90
91
|
--output <file> Name the exe to generate. Defaults to ./<scriptname>.exe.
|
|
91
92
|
--output-dir <dir> Output all files to a directory with a launch script instead of an exe.
|
|
92
93
|
--output-zip <file> Output a zip archive containing all files and a launch script.
|
|
94
|
+
--no-wrapper-exe Do not add the wrapper executable to installer, directory or
|
|
95
|
+
zip output (a launch script is always included there).
|
|
93
96
|
--macosx-bundle Build a macOS .app bundle. Use --output to name it (default: <scriptname>.app).
|
|
94
97
|
--bundle-id <id> Bundle identifier for the macOS app bundle (default: com.example.<appname>).
|
|
95
98
|
--no-lzma Disable LZMA compression of the executable.
|
|
@@ -125,6 +128,8 @@ EOF
|
|
|
125
128
|
when "--output-zip"
|
|
126
129
|
path = argv.shift
|
|
127
130
|
@options[:output_zip] = Pathname.new(path).expand_path if path
|
|
131
|
+
when "--no-wrapper-exe"
|
|
132
|
+
@options[:wrapper_exe?] = false
|
|
128
133
|
when "--macosx-bundle"
|
|
129
134
|
@options[:macosx_bundle?] = true
|
|
130
135
|
when "--bundle-id"
|
|
@@ -301,6 +306,8 @@ EOF
|
|
|
301
306
|
|
|
302
307
|
def quiet? = @options[__method__]
|
|
303
308
|
|
|
309
|
+
def wrapper_exe? = @options[__method__]
|
|
310
|
+
|
|
304
311
|
def rubyopt = @options[__method__]
|
|
305
312
|
|
|
306
313
|
def run_script? = @options[__method__]
|
data/lib/ocran/runner.rb
CHANGED
|
@@ -94,10 +94,12 @@ module Ocran
|
|
|
94
94
|
direction = Direction.new(@post_env, @pre_env, @option)
|
|
95
95
|
|
|
96
96
|
if @option.use_inno_setup?
|
|
97
|
-
|
|
97
|
+
# Native on Windows; on POSIX allow it when an ISCC command is
|
|
98
|
+
# available (e.g. via Wine wrapper scripts or in tests).
|
|
99
|
+
if Gem.win_platform? || system("command -v ISCC > /dev/null 2>&1")
|
|
98
100
|
direction.build_inno_setup_installer
|
|
99
101
|
else
|
|
100
|
-
raise "Inno Setup is only supported on Windows"
|
|
102
|
+
raise "Inno Setup is only supported on Windows (no ISCC command found in PATH)"
|
|
101
103
|
end
|
|
102
104
|
elsif @option.macosx_bundle
|
|
103
105
|
direction.build_macosx_bundle(@option.macosx_bundle)
|
data/lib/ocran/stub_builder.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Ocran
|
|
|
20
20
|
AUTO_CLEAN_INST_DIR = 0x04
|
|
21
21
|
CHDIR_BEFORE_SCRIPT = 0x08
|
|
22
22
|
DATA_COMPRESSED = 0x10
|
|
23
|
+
RUN_IN_EXE_DIR = 0x20
|
|
23
24
|
|
|
24
25
|
WINDOWS = Gem.win_platform?
|
|
25
26
|
|
|
@@ -27,7 +28,6 @@ module Ocran
|
|
|
27
28
|
STUB_PATH = File.expand_path(WINDOWS ? "stub.exe" : "stub", base_dir)
|
|
28
29
|
STUBW_PATH = WINDOWS ? File.expand_path("stubw.exe", base_dir) : nil
|
|
29
30
|
LZMA_PATH = WINDOWS ? File.expand_path("lzma.exe", base_dir) : nil
|
|
30
|
-
EDICON_PATH = WINDOWS ? File.expand_path("edicon.exe", base_dir) : nil
|
|
31
31
|
|
|
32
32
|
def self.find_posix_lzma_cmd
|
|
33
33
|
if system("which lzma > /dev/null 2>&1")
|
|
@@ -96,8 +96,16 @@ module Ocran
|
|
|
96
96
|
# icon_path:
|
|
97
97
|
# Specifies the path to the icon file to be embedded in the stub's resources.
|
|
98
98
|
#
|
|
99
|
+
# run_in_exe_dir:
|
|
100
|
+
# When set to true, the stub runs the application directly from its own
|
|
101
|
+
# directory instead of extracting to a temporary directory. Used for
|
|
102
|
+
# installer (Inno Setup) wrapper executables where the application files
|
|
103
|
+
# are installed next to the stub (pre-1.4/OCRA behavior). The directory
|
|
104
|
+
# is never deleted on exit.
|
|
105
|
+
#
|
|
99
106
|
def initialize(path, chdir_before: nil, debug_extract: nil, debug_mode: nil,
|
|
100
|
-
enable_compression: nil, gui_mode: nil, icon_path: nil
|
|
107
|
+
enable_compression: nil, gui_mode: nil, icon_path: nil,
|
|
108
|
+
run_in_exe_dir: nil)
|
|
101
109
|
@dirs = FilePathSet.new
|
|
102
110
|
@files = FilePathSet.new
|
|
103
111
|
@data_size = 0
|
|
@@ -122,14 +130,15 @@ module Ocran
|
|
|
122
130
|
|
|
123
131
|
# Embed icon resource (Windows only)
|
|
124
132
|
if icon_path && WINDOWS
|
|
125
|
-
|
|
133
|
+
require_relative "ed_icon"
|
|
134
|
+
EdIcon.update_icon(stub, icon_path.to_s)
|
|
126
135
|
end
|
|
127
136
|
|
|
128
137
|
File.open(stub, "ab") do |of|
|
|
129
138
|
@of = of
|
|
130
139
|
@opcode_offset = @of.size
|
|
131
140
|
|
|
132
|
-
write_header(debug_mode, debug_extract, chdir_before, enable_compression)
|
|
141
|
+
write_header(debug_mode, debug_extract, chdir_before, enable_compression, run_in_exe_dir)
|
|
133
142
|
|
|
134
143
|
b = proc {
|
|
135
144
|
yield(self)
|
|
@@ -213,14 +222,20 @@ module Ocran
|
|
|
213
222
|
end
|
|
214
223
|
private :compress
|
|
215
224
|
|
|
216
|
-
def write_header(debug_mode, debug_extract, chdir_before, compressed)
|
|
225
|
+
def write_header(debug_mode, debug_extract, chdir_before, compressed, run_in_exe_dir = nil)
|
|
217
226
|
next_to_exe, delete_after = debug_extract, !debug_extract
|
|
227
|
+
if run_in_exe_dir
|
|
228
|
+
# Wrapper mode: run in place next to the executable — never extract,
|
|
229
|
+
# and (critically) never delete the application directory on exit.
|
|
230
|
+
next_to_exe, delete_after = false, false
|
|
231
|
+
end
|
|
218
232
|
@of << [0 |
|
|
219
233
|
(debug_mode ? DEBUG_MODE : 0) |
|
|
220
234
|
(next_to_exe ? EXTRACT_TO_EXE_DIR : 0) |
|
|
221
235
|
(delete_after ? AUTO_CLEAN_INST_DIR : 0) |
|
|
222
236
|
(chdir_before ? CHDIR_BEFORE_SCRIPT : 0) |
|
|
223
|
-
(compressed ? DATA_COMPRESSED : 0)
|
|
237
|
+
(compressed ? DATA_COMPRESSED : 0) |
|
|
238
|
+
(run_in_exe_dir ? RUN_IN_EXE_DIR : 0)
|
|
224
239
|
].pack("C")
|
|
225
240
|
end
|
|
226
241
|
private :write_header
|
data/lib/ocran/version.rb
CHANGED
data/src/Makefile
CHANGED
|
@@ -21,7 +21,7 @@ else
|
|
|
21
21
|
STUB_CFLAGS := $(CFLAGS) -D_CONSOLE
|
|
22
22
|
STUBW_CFLAGS := $(CFLAGS)
|
|
23
23
|
SYSTEM_UTILS_SRC := system_utils.c
|
|
24
|
-
PROG_NAMES := stub stubw
|
|
24
|
+
PROG_NAMES := stub stubw
|
|
25
25
|
RESOURCE_OBJ := stub.res
|
|
26
26
|
endif
|
|
27
27
|
|
|
@@ -62,13 +62,11 @@ ifeq ($(IS_POSIX),)
|
|
|
62
62
|
stubw$(EXEEXT): $(COMMON_OBJS) $(WINDOW_OBJS)
|
|
63
63
|
$(CC) $(LDFLAGS) $(GUI_LDFLAGS) $^ $(LDLIBS) -o $@
|
|
64
64
|
|
|
65
|
-
edicon$(EXEEXT): edicon.o
|
|
66
|
-
$(CC) $(LDFLAGS) $^ -o $@
|
|
67
65
|
endif
|
|
68
66
|
|
|
69
67
|
clean:
|
|
70
68
|
rm -f $(BINARIES) $(COMMON_OBJS) $(CONSOLE_OBJS) $(WINDOW_OBJS) \
|
|
71
|
-
|
|
69
|
+
$(RESOURCE_OBJ)
|
|
72
70
|
|
|
73
71
|
install: $(BINARIES)
|
|
74
72
|
mkdir -p $(BINDIR)
|
data/src/inst_dir.c
CHANGED
|
@@ -98,8 +98,50 @@ const char *CreateInstDir(bool is_extract_to_exe_dir)
|
|
|
98
98
|
return NULL;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/* Normalize 8.3 short names (e.g. a TEMP under C:\Users\RUNNER~1) so all
|
|
102
|
+
paths derived from the extraction dir use one consistent spelling. */
|
|
103
|
+
char *long_dir = ToLongPath(inst_dir);
|
|
104
|
+
free(inst_dir);
|
|
105
|
+
if (!long_dir) {
|
|
106
|
+
return NULL;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
InstDir = long_dir;
|
|
110
|
+
return InstDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sets the installation directory to the executable's own directory
|
|
114
|
+
// (installer/wrapper mode, RUN_IN_EXE_DIR). No directory is created and
|
|
115
|
+
// the directory must never be deleted by the stub.
|
|
116
|
+
const char *SetInstDirToExeDir(void)
|
|
117
|
+
{
|
|
118
|
+
if (InstDir != NULL) {
|
|
119
|
+
APP_ERROR("Installation directory has already been set");
|
|
120
|
+
return NULL;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
char *image_path = GetImagePath();
|
|
124
|
+
if (!image_path) {
|
|
125
|
+
APP_ERROR("Failed to get executable name");
|
|
126
|
+
return NULL;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
char *image_dir = GetParentPath(image_path);
|
|
130
|
+
free(image_path);
|
|
131
|
+
if (!image_dir) {
|
|
132
|
+
APP_ERROR("Failed to obtain the directory path of the executable file");
|
|
133
|
+
return NULL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Normalize 8.3 short names for a consistent spelling (see CreateInstDir) */
|
|
137
|
+
char *long_dir = ToLongPath(image_dir);
|
|
138
|
+
free(image_dir);
|
|
139
|
+
if (!long_dir) {
|
|
140
|
+
return NULL;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
InstDir = long_dir;
|
|
144
|
+
return InstDir;
|
|
103
145
|
}
|
|
104
146
|
|
|
105
147
|
// Frees the allocated memory for the installation directory path.
|
data/src/inst_dir.h
CHANGED
|
@@ -20,6 +20,20 @@
|
|
|
20
20
|
*/
|
|
21
21
|
const char *CreateInstDir(bool is_extract_to_exe_dir);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @brief Sets the installation directory to the executable's own directory.
|
|
25
|
+
*
|
|
26
|
+
* Unlike CreateInstDir, no new directory is created: the stub runs the
|
|
27
|
+
* application directly from the directory the executable resides in
|
|
28
|
+
* (installer/wrapper mode, RUN_IN_EXE_DIR). The directory must never be
|
|
29
|
+
* deleted by the stub.
|
|
30
|
+
*
|
|
31
|
+
* @return
|
|
32
|
+
* A pointer to the directory path if successful, NULL if an error
|
|
33
|
+
* occurred. The returned path should not be freed by the caller.
|
|
34
|
+
*/
|
|
35
|
+
const char *SetInstDirToExeDir(void);
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* @brief Free the allocated installation directory path
|
|
25
39
|
* and reset the internal pointer to NULL.
|
data/src/stub.c
CHANGED
|
@@ -57,15 +57,24 @@ int main(int argc, char *argv[])
|
|
|
57
57
|
DEBUG("Ocran stub running in debug mode");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/* Create extraction directory
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/* Create extraction directory, or run in place next to the executable
|
|
61
|
+
(installer/wrapper mode, see RUN_IN_EXE_DIR) */
|
|
62
|
+
if (IsRunInExeDir(op_modes)) {
|
|
63
|
+
extract_dir = SetInstDirToExeDir();
|
|
64
|
+
if (!extract_dir) {
|
|
65
|
+
FATAL("Failed to resolve the executable directory");
|
|
66
|
+
goto cleanup;
|
|
67
|
+
}
|
|
68
|
+
DEBUG("Running in executable directory: %s", extract_dir);
|
|
69
|
+
} else {
|
|
70
|
+
extract_dir = CreateInstDir(IsExtractToExeDir(op_modes));
|
|
71
|
+
if (!extract_dir) {
|
|
72
|
+
FATAL("Failed to create extraction directory");
|
|
73
|
+
goto cleanup;
|
|
74
|
+
}
|
|
75
|
+
DEBUG("Created extraction directory: %s", extract_dir);
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
DEBUG("Created extraction directory: %s", extract_dir);
|
|
68
|
-
|
|
69
78
|
/* Unpacking process */
|
|
70
79
|
if (!ProcessImage(unpack_ctx)) {
|
|
71
80
|
FATAL("Failed to unpack image due to invalid or corrupted data");
|
|
@@ -120,7 +129,9 @@ cleanup:
|
|
|
120
129
|
/*
|
|
121
130
|
If AUTO_CLEAN_INST_DIR is set, delete the extraction directory.
|
|
122
131
|
*/
|
|
123
|
-
|
|
132
|
+
/* Never delete in RUN_IN_EXE_DIR mode: the "installation directory"
|
|
133
|
+
is the real application directory, not a temporary extraction dir. */
|
|
134
|
+
if (IsAutoCleanInstDir(op_modes) && !IsRunInExeDir(op_modes)) {
|
|
124
135
|
DEBUG("Deleting extraction directory: %s", extract_dir);
|
|
125
136
|
if (!DeleteInstDir()) {
|
|
126
137
|
DEBUG("Failed to delete extraction directory");
|
data/src/system_utils.c
CHANGED
|
@@ -96,6 +96,32 @@ char *JoinPath(const char *p1, const char *p2)
|
|
|
96
96
|
return joined_path;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
char *ToLongPath(const char *path)
|
|
100
|
+
{
|
|
101
|
+
if (!path) {
|
|
102
|
+
return NULL;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
DWORD required = GetLongPathName(path, NULL, 0);
|
|
106
|
+
if (required == 0) {
|
|
107
|
+
return _strdup(path); /* e.g. path does not exist - keep as is */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
char *buf = malloc(required);
|
|
111
|
+
if (!buf) {
|
|
112
|
+
APP_ERROR("Memory allocation failed for long path");
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
DWORD written = GetLongPathName(path, buf, required);
|
|
117
|
+
if (written == 0 || written >= required) {
|
|
118
|
+
free(buf);
|
|
119
|
+
return _strdup(path);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return buf;
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
char *GetParentPath(const char *path)
|
|
100
126
|
{
|
|
101
127
|
if (!path) {
|
data/src/system_utils.h
CHANGED
|
@@ -102,6 +102,20 @@ char *GetImagePath(void);
|
|
|
102
102
|
*/
|
|
103
103
|
char *GetTempDirectoryPath(void);
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @brief Normalizes a path to its long form.
|
|
107
|
+
*
|
|
108
|
+
* On Windows, converts 8.3 short names (e.g. "C:\\Users\\RUNNER~1") to the
|
|
109
|
+
* full long path via GetLongPathName. Two spellings of the same directory
|
|
110
|
+
* would otherwise defeat Ruby's $LOADED_FEATURES deduplication, causing
|
|
111
|
+
* files to be loaded twice ("already initialized constant" warnings).
|
|
112
|
+
* On POSIX this simply returns a copy of the given path.
|
|
113
|
+
*
|
|
114
|
+
* @return A newly allocated string (caller frees), or NULL on allocation
|
|
115
|
+
* failure. Falls back to a copy of the input if conversion fails.
|
|
116
|
+
*/
|
|
117
|
+
char *ToLongPath(const char *path);
|
|
118
|
+
|
|
105
119
|
/**
|
|
106
120
|
* @brief Writes the contents of a buffer to the specified file path.
|
|
107
121
|
* Creates any missing parent directories and overwrites existing files.
|
data/src/system_utils_posix.c
CHANGED
|
@@ -498,3 +498,9 @@ bool CreateAndWaitForProcess(const char *app_name, char *argv[], int *exit_code)
|
|
|
498
498
|
|
|
499
499
|
return true;
|
|
500
500
|
}
|
|
501
|
+
|
|
502
|
+
/* POSIX has no short-path aliases - return a plain copy. */
|
|
503
|
+
char *ToLongPath(const char *path)
|
|
504
|
+
{
|
|
505
|
+
return path ? strdup(path) : NULL;
|
|
506
|
+
}
|
data/src/unpack.c
CHANGED
|
@@ -536,6 +536,10 @@ bool IsDataCompressed(OperationModes modes) {
|
|
|
536
536
|
return IsMode(modes, DATA_COMPRESSED);
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
bool IsRunInExeDir(OperationModes modes) {
|
|
540
|
+
return IsMode(modes, RUN_IN_EXE_DIR);
|
|
541
|
+
}
|
|
542
|
+
|
|
539
543
|
bool ProcessImage(const UnpackContext *context)
|
|
540
544
|
{
|
|
541
545
|
if (!context) {
|
data/src/unpack.h
CHANGED
|
@@ -66,6 +66,14 @@ typedef enum {
|
|
|
66
66
|
* required before using the data.
|
|
67
67
|
*/
|
|
68
68
|
DATA_COMPRESSED = 0x10,
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Runs the application directly from the executable's own directory
|
|
72
|
+
* instead of creating an extraction directory. Used by installer builds
|
|
73
|
+
* (e.g. Inno Setup) where the application files are installed next to
|
|
74
|
+
* the stub executable. Restores the pre-1.4 (OCRA) wrapper behavior.
|
|
75
|
+
*/
|
|
76
|
+
RUN_IN_EXE_DIR = 0x20,
|
|
69
77
|
} OperationModes;
|
|
70
78
|
|
|
71
79
|
bool IsDebugMode(OperationModes modes);
|
|
@@ -73,6 +81,7 @@ bool IsExtractToExeDir(OperationModes modes);
|
|
|
73
81
|
bool IsAutoCleanInstDir(OperationModes modes);
|
|
74
82
|
bool IsChdirBeforeScript(OperationModes modes);
|
|
75
83
|
bool IsDataCompressed(OperationModes modes);
|
|
84
|
+
bool IsRunInExeDir(OperationModes modes);
|
|
76
85
|
|
|
77
86
|
typedef struct UnpackContext UnpackContext;
|
|
78
87
|
|