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.
@@ -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
- def gem_root_files = gem_root.find.select(&:file?)
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
- features_from_gems.select { |feature| feature.subpath?(gem_dir) }
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
- unless Gem.win_platform? || system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
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
- if Gem.win_platform?
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)
@@ -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
- system(EDICON_PATH, stub, icon_path.to_s, exception: true)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ocran
4
- VERSION = "1.4.2"
4
+ VERSION = "1.4.4"
5
5
  end
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 edicon
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
- edicon.o $(RESOURCE_OBJ)
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
- InstDir = inst_dir;
102
- return inst_dir;
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
- extract_dir = CreateInstDir(IsExtractToExeDir(op_modes));
62
- if (!extract_dir) {
63
- FATAL("Failed to create extraction directory");
64
- goto cleanup;
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
- if (IsAutoCleanInstDir(op_modes)) {
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.
@@ -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