ocran 1.4.3 → 1.4.5

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.
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/script_info.c CHANGED
@@ -163,7 +163,8 @@ static char **shallow_merge_argv(char *argv1[], char *argv2[])
163
163
  return outv;
164
164
  }
165
165
 
166
- bool RunScript(char *argv[], bool is_chdir_to_script_dir, int *exit_code)
166
+ bool RunScript(char *argv[], bool is_chdir_to_script_dir,
167
+ const char *chdir_dir, int *exit_code)
167
168
  {
168
169
  if (!IsScriptInfoSet()) {
169
170
  APP_ERROR("Script info is not initialized");
@@ -202,6 +203,7 @@ bool RunScript(char *argv[], bool is_chdir_to_script_dir, int *exit_code)
202
203
  goto cleanup;
203
204
  }
204
205
 
206
+ const char *target_dir = NULL;
205
207
  if (is_chdir_to_script_dir) {
206
208
  script_dir = GetParentPath(script_name);
207
209
  if (!script_dir) {
@@ -213,8 +215,19 @@ bool RunScript(char *argv[], bool is_chdir_to_script_dir, int *exit_code)
213
215
  "Changing working directory to script directory '%s'",
214
216
  script_dir
215
217
  );
218
+ target_dir = script_dir;
219
+ } else if (chdir_dir) {
220
+ DEBUG(
221
+ "Changing working directory to '%s'",
222
+ chdir_dir
223
+ );
224
+ target_dir = chdir_dir;
225
+ }
216
226
 
217
- char *ruby_optv[5] = { script_info[0], "-C", script_dir, "--", NULL };
227
+ if (target_dir) {
228
+ char *ruby_optv[5] = {
229
+ script_info[0], "-C", (char *)target_dir, "--", NULL
230
+ };
218
231
  char **new_argv = shallow_merge_argv(ruby_optv, merged_argv + 1);
219
232
  if (!new_argv) {
220
233
  APP_ERROR("Failed to merge script arguments with extra arguments");
data/src/script_info.h CHANGED
@@ -4,4 +4,18 @@
4
4
  char **GetScriptInfo(void);
5
5
  bool SetScriptInfo(const char *info, size_t info_size);
6
6
  void FreeScriptInfo(void);
7
- bool RunScript(char *argv[], bool is_chdir_to_script_dir, int *exit_code);
7
+ /**
8
+ * Launches the packaged script.
9
+ *
10
+ * @param argv Original argv of the stub; elements after
11
+ * argv[0] are appended to the script arguments.
12
+ * @param is_chdir_to_script_dir When true, the script starts with its working
13
+ * directory set to the extracted script's
14
+ * directory (--chdir-first).
15
+ * @param chdir_dir When non-NULL (and is_chdir_to_script_dir is
16
+ * false), the script starts with its working
17
+ * directory set to this path (--chdir-exe-dir).
18
+ * @param exit_code Receives the script's exit code.
19
+ */
20
+ bool RunScript(char *argv[], bool is_chdir_to_script_dir,
21
+ const char *chdir_dir, int *exit_code);
data/src/stub.c CHANGED
@@ -8,6 +8,10 @@
8
8
  #include <stdio.h>
9
9
  #include <stdbool.h>
10
10
  #include <stdlib.h>
11
+ #ifdef __COSMOPOLITAN__
12
+ #include <cosmo.h> /* IsWindows() */
13
+ #include <libc/nt/runtime.h> /* ExitProcess() */
14
+ #endif
11
15
  #include "error.h"
12
16
  #include "system_utils.h"
13
17
  #include "inst_dir.h"
@@ -21,6 +25,7 @@ int main(int argc, char *argv[])
21
25
  OperationModes op_modes = 0;
22
26
  const char *extract_dir = NULL;
23
27
  char *image_path = NULL;
28
+ char *exe_dir = NULL;
24
29
 
25
30
  /*
26
31
  Initialize signal and control handling so the parent process remains
@@ -57,15 +62,24 @@ int main(int argc, char *argv[])
57
62
  DEBUG("Ocran stub running in debug mode");
58
63
  }
59
64
 
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;
65
+ /* Create extraction directory, or run in place next to the executable
66
+ (installer/wrapper mode, see RUN_IN_EXE_DIR) */
67
+ if (IsRunInExeDir(op_modes)) {
68
+ extract_dir = SetInstDirToExeDir();
69
+ if (!extract_dir) {
70
+ FATAL("Failed to resolve the executable directory");
71
+ goto cleanup;
72
+ }
73
+ DEBUG("Running in executable directory: %s", extract_dir);
74
+ } else {
75
+ extract_dir = CreateInstDir(IsExtractToExeDir(op_modes));
76
+ if (!extract_dir) {
77
+ FATAL("Failed to create extraction directory");
78
+ goto cleanup;
79
+ }
80
+ DEBUG("Created extraction directory: %s", extract_dir);
65
81
  }
66
82
 
67
- DEBUG("Created extraction directory: %s", extract_dir);
68
-
69
83
  /* Unpacking process */
70
84
  if (!ProcessImage(unpack_ctx)) {
71
85
  FATAL("Failed to unpack image due to invalid or corrupted data");
@@ -87,12 +101,41 @@ int main(int argc, char *argv[])
87
101
  goto cleanup;
88
102
  }
89
103
 
104
+ /* Resolve the executable's directory when the script should start
105
+ with its working directory next to the .exe (--chdir-exe-dir). */
106
+ if (IsChdirToExeDir(op_modes)) {
107
+ exe_dir = GetParentPath(image_path);
108
+ if (!exe_dir) {
109
+ FATAL("Failed to resolve the executable directory");
110
+ goto cleanup;
111
+ }
112
+ DEBUG("Will start script in executable directory: %s", exe_dir);
113
+ }
114
+ #ifdef __COSMOPOLITAN__
115
+ /*
116
+ This stub is itself an APE. It fork/execs the payload interpreter and
117
+ reads the result with WEXITSTATUS() (system_utils_posix.c), so on
118
+ Windows the child must keep Cosmopolitan's wait-status exit encoding
119
+ (status << 8) -- otherwise `exit 3` would come back looking like death
120
+ by signal 3. CosmoRuby reports the plain status by default now, which
121
+ is right for a native parent but wrong for this one; the variable asks
122
+ it for the old encoding. Harmless for interpreters that do not know
123
+ the variable, and a no-op off Windows. This process still reports the
124
+ plain status to ITS native parent, at the end of main().
125
+ */
126
+ DEBUG("Set 'COSMORUBY_WAIT_STATUS_EXIT' so the payload's status survives waitpid()");
127
+ if (!SetEnvVar("COSMORUBY_WAIT_STATUS_EXIT", "1")) {
128
+ FATAL("The script cannot be launched due to a configuration error");
129
+ goto cleanup;
130
+ }
131
+ #endif
132
+
90
133
  /*
91
134
  RunScript uses the current value of status as its initial value
92
135
  and then overwrites it with the external script’s return code.
93
136
  */
94
137
  DEBUG("Run application script");
95
- if (!RunScript(argv, IsChdirBeforeScript(op_modes), &status)) {
138
+ if (!RunScript(argv, IsChdirBeforeScript(op_modes), exe_dir, &status)) {
96
139
  FATAL("Failed to run script");
97
140
  goto cleanup;
98
141
  }
@@ -110,6 +153,10 @@ cleanup:
110
153
  free(image_path);
111
154
  }
112
155
 
156
+ if (exe_dir) {
157
+ free(exe_dir);
158
+ }
159
+
113
160
  if (unpack_ctx) {
114
161
  ClosePackFile(unpack_ctx);
115
162
  unpack_ctx = NULL;
@@ -120,7 +167,9 @@ cleanup:
120
167
  /*
121
168
  If AUTO_CLEAN_INST_DIR is set, delete the extraction directory.
122
169
  */
123
- if (IsAutoCleanInstDir(op_modes)) {
170
+ /* Never delete in RUN_IN_EXE_DIR mode: the "installation directory"
171
+ is the real application directory, not a temporary extraction dir. */
172
+ if (IsAutoCleanInstDir(op_modes) && !IsRunInExeDir(op_modes)) {
124
173
  DEBUG("Deleting extraction directory: %s", extract_dir);
125
174
  if (!DeleteInstDir()) {
126
175
  DEBUG("Failed to delete extraction directory");
@@ -129,5 +178,17 @@ cleanup:
129
178
 
130
179
  FreeInstDir();
131
180
  extract_dir = NULL;
181
+
182
+ #ifdef __COSMOPOLITAN__
183
+ /* On Windows, Cosmopolitan Libc's exit path encodes the full wait
184
+ status into the process exit code (code << 8) so that cosmo
185
+ parents can reconstruct it. Native Windows parents (cmd,
186
+ PowerShell, CI runners) expect the plain code, so bypass the
187
+ libc exit path and report the code as-is. */
188
+ if (IsWindows()) {
189
+ fflush(NULL);
190
+ ExitProcess(status);
191
+ }
192
+ #endif
132
193
  return status;
133
194
  }
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.
@@ -1,6 +1,9 @@
1
1
  #ifdef __APPLE__
2
2
  #include <mach-o/dyld.h>
3
3
  #endif
4
+ #ifdef __COSMOPOLITAN__
5
+ #include <cosmo.h> /* GetProgramExecutableName() */
6
+ #endif
4
7
  #include <unistd.h>
5
8
  #include <fcntl.h>
6
9
  #include <sys/stat.h>
@@ -360,6 +363,23 @@ bool ExportFile(const char *path, const void *buffer, size_t buffer_size) {
360
363
  /* ===== Path utilities ===== */
361
364
 
362
365
  char *GetImagePath(void) {
366
+ #ifdef __COSMOPOLITAN__
367
+ /* Cosmopolitan Libc defines neither __linux__ nor __APPLE__; it resolves
368
+ the executable path itself on every OS the APE binary runs on. */
369
+ const char *exe = GetProgramExecutableName();
370
+ if (!exe || !*exe) {
371
+ FATAL("GetImagePath: GetProgramExecutableName failed");
372
+ return NULL;
373
+ }
374
+
375
+ char *result = strdup(exe);
376
+ if (!result) {
377
+ FATAL("GetImagePath: strdup failed");
378
+ return NULL;
379
+ }
380
+
381
+ return result;
382
+ #else
363
383
  static char path_buffer[4096];
364
384
  #ifdef __APPLE__
365
385
  uint32_t size = sizeof(path_buffer);
@@ -388,10 +408,30 @@ char *GetImagePath(void) {
388
408
 
389
409
  strcpy(result, path_buffer);
390
410
  return result;
411
+ #endif /* __COSMOPOLITAN__ */
391
412
  }
392
413
 
393
414
  char *GetTempDirectoryPath(void) {
394
415
  const char *tmpdir = getenv("TMPDIR");
416
+ #ifdef __COSMOPOLITAN__
417
+ /* On Windows, Cosmopolitan Libc translates the magic "/tmp" prefix to
418
+ a real directory, but WHERE it points differs between cosmo
419
+ releases (%TMP% in some, C:\tmp in others). This stub and the
420
+ packed cosmopolitan Ruby may be built against different cosmo
421
+ versions, so a literal "/tmp/..." extraction path handed to the
422
+ child can resolve to a DIFFERENT directory than the one the stub
423
+ extracted into. TMP/TEMP, by contrast, are presented by cosmo in
424
+ its unambiguous drive-letter form (e.g. /C/Users/x/AppData/Local/
425
+ Temp) that every cosmo runtime resolves identically, so prefer
426
+ them. On POSIX hosts they are normally unset and the usual
427
+ TMPDIR -> /tmp behavior is preserved. */
428
+ if (!tmpdir || !*tmpdir) {
429
+ tmpdir = getenv("TMP");
430
+ }
431
+ if (!tmpdir || !*tmpdir) {
432
+ tmpdir = getenv("TEMP");
433
+ }
434
+ #endif
395
435
  if (!tmpdir || !*tmpdir) {
396
436
  tmpdir = "/tmp";
397
437
  }
@@ -498,3 +538,9 @@ bool CreateAndWaitForProcess(const char *app_name, char *argv[], int *exit_code)
498
538
 
499
539
  return true;
500
540
  }
541
+
542
+ /* POSIX has no short-path aliases - return a plain copy. */
543
+ char *ToLongPath(const char *path)
544
+ {
545
+ return path ? strdup(path) : NULL;
546
+ }
data/src/unpack.c CHANGED
@@ -536,6 +536,14 @@ 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
+
543
+ bool IsChdirToExeDir(OperationModes modes) {
544
+ return IsMode(modes, CHDIR_TO_EXE_DIR);
545
+ }
546
+
539
547
  bool ProcessImage(const UnpackContext *context)
540
548
  {
541
549
  if (!context) {
data/src/unpack.h CHANGED
@@ -66,6 +66,23 @@ 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,
77
+
78
+ /**
79
+ * Change the current directory to the directory containing the
80
+ * executable before running the script. This makes relative file
81
+ * access resolve next to the .exe, regardless of how it was invoked
82
+ * (Explorer, command prompt, file association). Opt-in via the
83
+ * --chdir-exe-dir build option.
84
+ */
85
+ CHDIR_TO_EXE_DIR = 0x40,
69
86
  } OperationModes;
70
87
 
71
88
  bool IsDebugMode(OperationModes modes);
@@ -73,6 +90,8 @@ bool IsExtractToExeDir(OperationModes modes);
73
90
  bool IsAutoCleanInstDir(OperationModes modes);
74
91
  bool IsChdirBeforeScript(OperationModes modes);
75
92
  bool IsDataCompressed(OperationModes modes);
93
+ bool IsRunInExeDir(OperationModes modes);
94
+ bool IsChdirToExeDir(OperationModes modes);
76
95
 
77
96
  typedef struct UnpackContext UnpackContext;
78
97
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocran
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andi Idogawa
@@ -25,27 +25,13 @@ dependencies:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
27
  version: '1.0'
28
- description: |
29
- OCRAN (One-Click Ruby Application Next) packages Ruby applications for
30
- distribution. It bundles your script, the Ruby interpreter, gems, and native
31
- libraries into a self-contained artifact that runs without requiring Ruby to
32
- be installed on the target machine.
33
-
34
- Three output formats are supported on all platforms:
35
- - Self-extracting executable (.exe on Windows, native binary on Linux/macOS)
36
- - Directory with a launch script (--output-dir)
37
- - Zip archive with a launch script (--output-zip)
38
-
39
- This is a fork of OCRA maintained for Ruby 3.2+ compatibility.
40
- Migration guide: replace OCRA_EXECUTABLE with OCRAN_EXECUTABLE in your code.
41
-
42
- Usage:
43
- ocran helloworld.rb # builds helloworld.exe / helloworld
44
- ocran --output-dir out/ app.rb
45
- ocran --output-zip app.zip app.rb
46
-
47
- See readme at https://github.com/largo/ocran
48
- Report problems at https://github.com/largo/ocran/issues
28
+ description: 'OCRAN (One-Click Ruby Application Next) packages Ruby applications for
29
+ distribution on Windows, Linux, and macOS. It bundles your script, the Ruby interpreter,
30
+ gems, and native libraries into a self-contained artifact that runs without Ruby
31
+ installed on the target machine: a self-extracting executable (default), a directory
32
+ (--output-dir), a zip archive (--output-zip), or a macOS .app bundle (--macosx-bundle).
33
+ Fork of OCRA maintained for Ruby 3.2+; migrating code should replace OCRA_EXECUTABLE
34
+ with OCRAN_EXECUTABLE.'
49
35
  email:
50
36
  - andi@idogawa.com
51
37
  executables:
@@ -64,6 +50,7 @@ files:
64
50
  - lib/ocran/build_facade.rb
65
51
  - lib/ocran/build_helper.rb
66
52
  - lib/ocran/command_output.rb
53
+ - lib/ocran/cosmo_toolchain.rb
67
54
  - lib/ocran/dir_builder.rb
68
55
  - lib/ocran/direction.rb
69
56
  - lib/ocran/ed_icon.rb
@@ -73,15 +60,19 @@ files:
73
60
  - lib/ocran/host_config_helper.rb
74
61
  - lib/ocran/inno_setup_script_builder.rb
75
62
  - lib/ocran/launcher_batch_builder.rb
63
+ - lib/ocran/launcher_event_recorder.rb
76
64
  - lib/ocran/library_detector.rb
77
65
  - lib/ocran/library_detector_posix.rb
78
66
  - lib/ocran/option.rb
79
67
  - lib/ocran/refine_pathname.rb
68
+ - lib/ocran/rubyopt_processor.rb
80
69
  - lib/ocran/runner.rb
81
70
  - lib/ocran/runtime_environment.rb
82
71
  - lib/ocran/stub_builder.rb
83
72
  - lib/ocran/version.rb
84
73
  - lib/ocran/windows_command_escaping.rb
74
+ - lib/ocran/zip_payload_builder.rb
75
+ - lib/ocran/zip_writer.rb
85
76
  - share/ocran/lzma.exe
86
77
  - src/Makefile
87
78
  - src/error.c
@@ -110,7 +101,7 @@ licenses:
110
101
  metadata:
111
102
  homepage_uri: https://github.com/largo/ocran
112
103
  source_code_uri: https://github.com/largo/ocran
113
- changelog_uri: https://github.com/largo/ocran/CHANGELOG.txt
104
+ changelog_uri: https://github.com/largo/ocran/blob/master/CHANGELOG.txt
114
105
  rdoc_options: []
115
106
  require_paths:
116
107
  - lib
@@ -125,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
116
  - !ruby/object:Gem::Version
126
117
  version: '0'
127
118
  requirements: []
128
- rubygems_version: 4.0.6
119
+ rubygems_version: 4.0.16
129
120
  specification_version: 4
130
121
  summary: OCRAN (One-Click Ruby Application Next) packages Ruby applications for distribution
131
122
  on Windows, Linux, and macOS.