ocran 1.4.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23bee04c3290671dc55673fcb757a222e04af6977bd015a0791bab76ef492f1c
4
- data.tar.gz: a4d437bda8e4bd00ea4a66c9c75c2aaa85b6904b8c7619e4f04a5e2b931688d5
3
+ metadata.gz: 79aa33b2625758b6cce7b1175d45d73c5c60209b996a6d5ab03a4df55acbae28
4
+ data.tar.gz: 3d53ff8c8b7a31448080f105f33af4968dea8f9a8fd559aa92a9e87db31db0dd
5
5
  SHA512:
6
- metadata.gz: 0bd96938ab10206fe5009d93d9356034cbb9913159507b51f0df1fa4d46f428fbf7ae0c75ce1e8359079af70094ab8b68d64671064a59ec8bda313baf863dc79
7
- data.tar.gz: 199615b364954980946418e4c850e9153cf2a2aabf4a5475143e08ff8bbeeda4dcb784a2fb578ef07b9559b8ae37b24103b36f99e4f86c96357674a115c371a6
6
+ metadata.gz: e9975e649595c77397e49649544fad56b209917a55f7ea6d7ff7acf748a14608165013fbe1e43908946b0cba1badbae22da7b73aac4ba2661fb092162d333386
7
+ data.tar.gz: 67007f4112f91a8bb6a01564a8b5b4b54edd2b852a629f78d1bc12bd9062e3a71771fe0a802bc9f3133560cd0aa7da18fa912f0eb0f2f8337288ea226be308b2
data/CHANGELOG.txt CHANGED
@@ -1,3 +1,22 @@
1
+ === 1.4.4
2
+ - Fix crash on distro-packaged and Homebrew Ruby when a default gem (e.g. fiddle, singleton) has a gemspec but no materialized gem directory: file collection now treats the missing directory as an empty set instead of raising Errno::ENOENT from Find.find (issue #44). The gem's files, which live in the stdlib, are still packed via the load path.
3
+ - Pack the real Ruby interpreter when bindir/ruby is a dispatcher script (e.g. Fedora's rubypick): the running interpreter binary (/proc/self/exe) is packed under the expected name instead, so the packed executable no longer requires Ruby on the target system.
4
+ - Build runtime RUBYLIB from the build Ruby's actual default load path in addition to the RbConfig directories, covering distro-specific compiled-in entries such as Fedora's /usr/share/rubygems where rubygems.rb lives outside rubylibdir.
5
+ - Include every Gem.path entry under the Ruby prefix in the runtime GEM_PATH (not just Gem.default_dir), so RubyGems can activate gems from split layouts like Fedora's /usr/share/gems + /usr/local/share/gems.
6
+ - Make default gems activatable at runtime: their gemspecs (from specifications/default/, which RubyGems only scans under the compiled-in Gem.default_dir) are also copied into the regular specifications directory of the same packed base directory.
7
+ - Handle distros that expose gem files under additional symlinked paths (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the psych gem directory): loaded features are resolved by realpath so they are packed inside the gem directory, and duplicate stdlib-level copies that would shadow the packed gem at runtime are no longer packed.
8
+ - Pack native extension files that distros build outside the gem's base_dir (e.g. Fedora's /usr/lib64/gems/ruby) into the extension directory RubyGems computes at runtime, and recreate the gem.build_complete marker there so the gem activates instead of being ignored ("its extensions are not built").
9
+ - Linux: bundle detected non-glibc shared libraries (libyaml, libssl, libcrypt, libgmp, libz, ...) next to libruby, with soname symlink aliases, so packed executables also run on other distros where these are missing or have different sonames. Core glibc libraries, the loader, and libnss_* plugins are never bundled; the target's glibc must be at least as new as the build host's. Disable with --no-autodll.
10
+ - CI: new isolated workflow test-aspera-cli.yml that packages the aspera-cli gem with distro Ruby on Fedora and verifies the executable runs on clean Fedora and Ubuntu containers without Ruby. Kept separate from the release pipeline with a read-only token; the third-party gem only ever runs inside throwaway containers.
11
+ - Restore the OCRA-compatible wrapper executable for Inno Setup builds: `--innosetup` now additionally builds a small stub executable named after `--output` and installs it into `{app}` alongside the application files. The wrapper runs the installed application directly from its own directory (new RUN_IN_EXE_DIR stub mode) — no extraction, no cleanup of the application directory. This behavior existed in OCRA and ocran <= 1.3.x but was lost in the 1.4.0 rewrite, breaking user ISS scripts that reference the executable, e.g. in [Run]/[UninstallRun] entries or for Windows service registration (`myapp.exe --register-service`). The launcher.bat is still generated and installed as before.
12
+ - Add `run_in_exe_dir` option to StubBuilder and a RUN_IN_EXE_DIR (0x20) stub header flag. In this mode the stub never deletes its directory (double-guarded in the stub itself, since the directory is the real application directory).
13
+ - Allow `--innosetup` on POSIX hosts when an ISCC command is available in PATH (e.g. a Wine wrapper); previously it was rejected on non-Windows unconditionally. Fix the ISCC presence check to use `command -v` instead of the Windows-only `where`/`>NUL` on POSIX.
14
+ - Inno Setup builds from POSIX hosts no longer crash on symlink entries (symlinks cannot be expressed in an installer script and are skipped; they are only generated for POSIX shared-library aliases).
15
+ - `--output-dir` and `--output-zip` now also include the wrapper executable (named after `--output`) next to the launch script, so the unpacked output has a doubleclickable, pinnable, signable entry point that can even be registered as a Windows service. The launch script (`.bat`/`.sh`) is still generated.
16
+ - New `--no-wrapper-exe` option: omit the wrapper executable from installer, directory and zip output.
17
+ - Always create the packed prefix gem directories (e.g. lib/ruby/gems/<api>) even when empty: RubyGems probes the default gem dir for writability at startup and printed "Can't determine writability of default gem path" on stderr for every packed executable on newer RubyGems.
18
+ - Stub: normalize the extraction/application directory to its long path form on Windows (GetLongPathName). With an 8.3 short TEMP path (e.g. C:\Users\RUNNER~1) the same file could be loaded under two spellings, defeating $LOADED_FEATURES deduplication and causing "already initialized constant" warnings.
19
+
1
20
  === 1.4.3
2
21
  - Migrate edicon from C to Ruby: replace edicon.exe with a pure-Ruby implementation (ed_icon.rb), fixing incorrect BeginUpdateResource error checking and wrong GroupIconSize calculation. Shortens build times and removes the C binary from the build.
3
22
  - Include Gem.default_dir in GEM_PATH on all platforms: previously the exec_prefix gem directory was excluded on Windows, causing native-extension gems (e.g. fxruby/fox16) to fail at runtime with a LoadError even though they were bundled correctly.
data/README.md CHANGED
@@ -1,11 +1,6 @@
1
- # ocran
1
+ # OCRAN
2
2
 
3
- home :: https://github.com/largo/ocran/
4
- issues :: http://github.com/largo/ocran/issues
5
-
6
- ## Description
7
-
8
- OCRAN (One-Click Ruby Application Next) packages Ruby applications for
3
+ [OCRAN (One-Click Ruby Application Next)](https://github.com/largo/ocran) packages Ruby applications for
9
4
  distribution. It bundles your script, the Ruby interpreter, gems, and native
10
5
  libraries into a single self-contained artifact that runs without requiring
11
6
  Ruby to be installed on the target machine.
@@ -20,6 +15,8 @@ OCRAN supports four output formats, all cross-platform:
20
15
  OCRAN is a fork of [OCRA](https://github.com/larsch/ocra) maintained for
21
16
  Ruby 3.2+ compatibility.
22
17
 
18
+ If you run into errors while using OCRAN, please check the [OCRAN issues](https://github.com/largo/ocran/issues) first.
19
+
23
20
  ## Recommended usage
24
21
 
25
22
  The most common use-case is shipping a program to users running Windows / Linux / macOS
@@ -34,7 +31,8 @@ If using Windows you can use the Inno Setup
34
31
  option (`--innosetup`) to produce a proper installer that extracts once to a
35
32
  permanent directory.
36
33
 
37
- You can easily generate binaries for the supported Operating Systems with GitHub Actions.
34
+ You can easily generate binaries for the supported Operating Systems with GitHub
35
+ Actions — see [Building for multiple platforms with GitHub Actions](#building-for-multiple-platforms-with-github-actions).
38
36
 
39
37
  ## Features
40
38
 
@@ -238,7 +236,92 @@ OCRAN on the same platform (and architecture) as the intended target**:
238
236
  There is no support for building a Windows `.exe` from a Linux or macOS host,
239
237
  or vice versa. If you need builds for multiple platforms, run OCRAN in CI on
240
238
  each target platform separately (e.g., a Windows runner for `.exe` builds and
241
- a Linux runner for Linux builds).
239
+ a Linux runner for Linux builds). See
240
+ [Building for multiple platforms with GitHub Actions](#building-for-multiple-platforms-with-github-actions)
241
+ for a ready-made setup.
242
+
243
+ ### Linux binary portability (glibc)
244
+
245
+ On Linux, OCRAN bundles the shared libraries your application loaded during
246
+ the build (libyaml, libssl, libcrypt, libgmp, libz, ...) next to the bundled
247
+ Ruby, so the executable also runs on distributions where those libraries are
248
+ missing or have different sonames. It works with distro-packaged Ruby too
249
+ (e.g. `dnf install ruby` on Fedora), including its split gem layout and
250
+ `rubypick` wrapper.
251
+
252
+ One thing can never be bundled: **glibc itself** (and its dynamic loader).
253
+ Those always come from the target system, and glibc is only
254
+ backward-compatible. In practice:
255
+
256
+ * A Linux binary runs on any distribution whose glibc is **at least as new**
257
+ as the build machine's.
258
+ * Build on the **oldest** distribution you want to support. For example, a
259
+ binary built on the oldest available `ubuntu-*` GitHub runner runs on that
260
+ Ubuntu version and everything newer, while a binary built on a
261
+ cutting-edge distribution (e.g. `fedora:latest`) will not run on older
262
+ Debian/Ubuntu releases (`` version `GLIBC_2.4x' not found ``).
263
+
264
+ Use `--no-autodll` to disable shared library bundling.
265
+
266
+ ## Building for multiple platforms with GitHub Actions
267
+
268
+ Since OCRAN is not a cross-compiler, the recommended way to ship your app for
269
+ Windows, Linux, and macOS is a GitHub Actions matrix that runs OCRAN natively
270
+ on each platform. A minimal workflow:
271
+
272
+ ```yaml
273
+ name: Build binaries
274
+
275
+ on:
276
+ push:
277
+ tags: ['v*'] # build releases from version tags
278
+ workflow_dispatch: # allow manual runs
279
+
280
+ jobs:
281
+ build:
282
+ strategy:
283
+ fail-fast: false
284
+ matrix:
285
+ include:
286
+ - os: windows-latest # x86-64 Windows .exe
287
+ - os: ubuntu-22.04 # x86-64 Linux; oldest runner = widest glibc compatibility
288
+ - os: macos-14 # Apple Silicon macOS
289
+ - os: macos-15-intel # Intel macOS
290
+ runs-on: ${{ matrix.os }}
291
+ steps:
292
+ - uses: actions/checkout@v4
293
+
294
+ - uses: ruby/setup-ruby@v1
295
+ with:
296
+ ruby-version: '3.4'
297
+ bundler-cache: true # if your app has a Gemfile
298
+
299
+ - run: gem install ocran
300
+
301
+ # Run OCRAN on your entry script. Add "-- <args>" if your script needs
302
+ # arguments to exit cleanly during the dependency-detection run.
303
+ - run: ocran myapp.rb --output myapp
304
+ # produces myapp.exe on Windows, myapp on Linux/macOS
305
+
306
+ - uses: actions/upload-artifact@v4
307
+ with:
308
+ name: myapp-${{ matrix.os }}
309
+ path: myapp*
310
+ ```
311
+
312
+ Notes:
313
+
314
+ * Pick the **oldest** `ubuntu-*` runner GitHub offers for the Linux build so
315
+ the binary runs on as many distributions as possible (see
316
+ [Linux binary portability](#linux-binary-portability-glibc)).
317
+ * On macOS, each architecture needs its own build: `macos-14` (and newer
318
+ numbered ARM runners) produce Apple Silicon binaries, `macos-15-intel`
319
+ produces Intel binaries.
320
+ * Swap the `ocran` invocation for `--output-dir`, `--output-zip`,
321
+ `--macosx-bundle`, or `--innosetup` depending on how you want to ship
322
+ (see [Recommended usage](#recommended-usage)).
323
+ * For tag pushes you can attach the artifacts to a GitHub release with a
324
+ follow-up job (e.g. `softprops/action-gh-release`).
242
325
 
243
326
  ## Requirements
244
327
 
@@ -320,8 +403,12 @@ If your script uses `Kernel#autoload`, OCRAN will attempt to load those
320
403
  constants so they are included in the output. Missing modules are ignored
321
404
  with a warning.
322
405
 
323
- Dynamic link libraries (.dll files, for example WxWidgets, or other
324
- source files) will be detected and included by OCRAN.
406
+ Dynamic link libraries are detected and included by OCRAN: `.dll` files on
407
+ Windows (for example WxWidgets), and on Linux the non-glibc shared libraries
408
+ the process loaded (libyaml, libssl, libcrypt, ...), which are placed next to
409
+ the bundled Ruby with appropriate soname symlinks (see
410
+ [Linux binary portability](#linux-binary-portability-glibc)). Disable with
411
+ `--no-autodll`.
325
412
 
326
413
  ### Including libraries non-automatically
327
414
 
@@ -534,10 +621,12 @@ file:
534
621
 
535
622
  ## Credits
536
623
 
537
- Lars Christensen and contributors for the OCRA project which this is forked from.
624
+ [Andi Idogawa](https://github.com/largo), [Shinokaro](https://github.com/shinokaro)
625
+ [Lars Christensen](https://github.com/larsch) and contributors for the OCRA project which this is forked from.
626
+
627
+ Special thanks to Ruby on Windows maintainers such as [Nobuyoshi Nakada](https://github.com/nobu), [Lars Kanis](https://github.com/larskanis), and [MSP-Greg](https://github.com/MSP-Greg)
538
628
 
539
- Kevin Walzer of codebykevin, Maxim Samsonov for ocra2, John Mair for
540
- codesigning support.
629
+ [Kevin Walzer](https://github.com/codebykevin) of codebykevin, [Maxim Samsonov](https://github.com/maxirmx), and [John Mair](https://github.com/banister) for codesigning support.
541
630
 
542
631
  Igor Pavlov for the LZMA compressor and decompressor (Public Domain).
543
632
 
@@ -551,7 +640,7 @@ http://ruby.morphball.net/vit-ruby-ico_en.html).
551
640
  (The MIT License)
552
641
 
553
642
  Copyright (c) 2009-2020 Lars Christensen
554
- Copyright (c) 2020-2025 The OCRAN Committers Team
643
+ Copyright (c) 2020-2026 The OCRAN Committers Team
555
644
 
556
645
  Permission is hereby granted, free of charge, to any person obtaining
557
646
  a copy of this software and associated documentation files (the
@@ -10,6 +10,8 @@ module Ocran
10
10
 
11
11
  def cp(...) = @filer.__send__(__method__, ...)
12
12
 
13
+ def symlink(...) = @filer.__send__(__method__, ...)
14
+
13
15
  def export(...) = @launcher.__send__(__method__, ...)
14
16
 
15
17
  def exec(...) = @launcher.__send__(__method__, ...)
@@ -13,6 +13,9 @@ module Ocran
13
13
  WINDOWS = Gem.win_platform?
14
14
 
15
15
  attr_reader :data_size
16
+ # Recorded launch data (environment variables and exec arguments), used
17
+ # by Direction to build the optional wrapper executable.
18
+ attr_reader :env, :exec_args
16
19
 
17
20
  def initialize(path)
18
21
  @path = Pathname(path)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "rbconfig"
3
3
  require "pathname"
4
+ require "set"
4
5
  require_relative "refine_pathname"
5
6
  require_relative "host_config_helper"
6
7
  require_relative "command_output"
@@ -17,6 +18,12 @@ module Ocran
17
18
  # - /ruby/vendor_ruby/3.0.0/
18
19
  RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+)/?$}i
19
20
 
21
+ # Core libraries provided by glibc and the dynamic loader. These must
22
+ # never be bundled: they are tightly coupled to the target system's
23
+ # ld.so, and shipping a foreign copy breaks the dynamic linker. The
24
+ # libnss_* plugins are dlopened by the target's glibc and must match it.
25
+ LINUX_SYSTEM_LIBRARY_RE = /\A(?:ld-linux|ld64|libc\.so|libm\.so|libdl\.so|libpthread\.so|librt\.so|libutil\.so|libnsl\.so|libresolv\.so|libmvec\.so|libanl\.so|libBrokenLocale\.so|libnss_)/
26
+
20
27
  include BuildConstants, CommandOutput, HostConfigHelper
21
28
 
22
29
  attr_reader :ruby_executable, :rubyopt
@@ -168,7 +175,20 @@ module Ocran
168
175
 
169
176
  # Add the ruby executable and DLL
170
177
  say "Adding ruby executable #{ruby_executable}"
171
- builder.copy_to_bin(bindir / ruby_executable, ruby_executable)
178
+ ruby_source = bindir / ruby_executable
179
+ if !Gem.win_platform? && File.binread(ruby_source, 2) == "#!"
180
+ # On some distros (e.g. Fedora), bindir/ruby is a dispatcher shell
181
+ # script ("rubypick") rather than the interpreter itself, which
182
+ # cannot run on a system without Ruby. Pack the currently running
183
+ # interpreter binary under the expected name instead.
184
+ real_ruby = Pathname("/proc/self/exe")
185
+ raise "#{ruby_source} is a wrapper script and the real interpreter could not be determined" unless real_ruby.exist?
186
+
187
+ say "#{ruby_source} is a wrapper script; packing #{real_ruby.realpath} instead"
188
+ builder.copy_to_bin(real_ruby.realpath, ruby_executable)
189
+ else
190
+ builder.copy_to_bin(ruby_source, ruby_executable)
191
+ end
172
192
  if libruby_so
173
193
  # On POSIX systems, libruby.so is in libdir; on Windows, it's in bindir
174
194
  libruby_src = Gem.win_platform? ? bindir / libruby_so : libdir / libruby_so
@@ -221,6 +241,44 @@ module Ocran
221
241
  end
222
242
  end
223
243
 
244
+ # Linux: bundle detected shared libraries (e.g. libyaml, libssl,
245
+ # libcrypt) next to libruby so the executable also runs on systems
246
+ # where they are missing or have different sonames (the stub already
247
+ # points LD_LIBRARY_PATH at the packed bin directory). Core glibc
248
+ # libraries and the loader are never bundled - they must come from the
249
+ # target system. Ruby native extensions are packed as features, not
250
+ # here.
251
+ if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls?
252
+ feature_set = features.to_set
253
+ feature_realpaths = features.filter_map { |f| f.realpath rescue nil }.to_set
254
+ # Ruby native extensions live in these directories and are packed as
255
+ # features; they must not be duplicated into bin.
256
+ ruby_arch_dirs = RbConfig::CONFIG.values_at("archdir", "sitearchdir", "vendorarchdir")
257
+ .compact.map { |dir| Pathname(dir) }
258
+ detect_dlls.each do |dll|
259
+ basename = dll.basename.to_s
260
+ next unless basename.match?(/\.so(\.|\z)/)
261
+ next if basename.match?(LINUX_SYSTEM_LIBRARY_RE)
262
+ # libruby is packed with its aliases already
263
+ next if basename.start_with?("libruby")
264
+ next if feature_set.include?(dll) || feature_realpaths.include?(dll)
265
+ next if ruby_arch_dirs.any? { |dir| dll.subpath?(dir) } || dll.to_posix.match?(%r{/gems/})
266
+ next unless dll.file?
267
+
268
+ say "Adding detected shared library #{dll}"
269
+ builder.copy_to_bin(dll, basename)
270
+ # The loader may request a library by a less specific soname than
271
+ # the fully versioned file name recorded in the memory map (e.g.
272
+ # libssl.so.3 for libssl.so.3.2.4). Provide symlink aliases for
273
+ # each shorter version suffix.
274
+ alias_name = basename
275
+ while (shorter = alias_name.sub(/\.\d+[^.]*\z/, "")) != alias_name && shorter.include?(".so")
276
+ builder.symlink_in_bin(basename, shorter)
277
+ alias_name = shorter
278
+ end
279
+ end
280
+ end
281
+
224
282
  # Windows-only: Add external manifest and builtin DLLs
225
283
  if Gem.win_platform?
226
284
  if (manifest = ruby_builtin_manifest)
@@ -284,6 +342,45 @@ module Ocran
284
342
  raise "Gem spec #{spec_file} does not exist in the Ruby installation. Don't know where to put it."
285
343
  end
286
344
 
345
+ spec_dir = spec_file.dirname
346
+ default_spec = spec_dir.basename.to_s == "default"
347
+ # The gem's base directory: specs live in <base_dir>/specifications[/default]
348
+ base_dir = default_spec ? spec_dir.dirname.dirname : spec_dir.dirname
349
+ # Relative packed location of base_dir; nil when the gem lives outside
350
+ # the Ruby prefix, in which case its files are packed under GEMDIR
351
+ # with the same layout.
352
+ packed_base = base_dir.subpath?(exec_prefix) ? base_dir.relative_path_from(exec_prefix) : GEMDIR
353
+
354
+ # Default gemspecs live in a "specifications/default" directory, which
355
+ # RubyGems only scans under Gem.default_dir. On rubies with a
356
+ # compiled-in absolute prefix (e.g. distro Ruby on Fedora), that path
357
+ # points to the build host and does not exist on the target system, so
358
+ # activating such a gem (e.g. via a binstub) fails. Also place a copy
359
+ # in the regular specifications directory of the same packed base
360
+ # directory, where GEM_PATH makes it discoverable and its require
361
+ # paths resolve to the packed gem files.
362
+ if !Gem.win_platform? && default_spec
363
+ builder.cp(spec_file, packed_base / "specifications" / spec_file.basename)
364
+ end
365
+
366
+ # Native-extension gems record successful builds in a gem.build_complete
367
+ # marker under <base_dir>/extensions/...; without it RubyGems refuses to
368
+ # activate the gem ("Ignoring x because its extensions are not built").
369
+ # The marker can be absent from the packed tree either because the
370
+ # distro relocates it (e.g. Fedora keeps it under /usr/lib64/gems/ruby
371
+ # via a default_ext_dir_for override) or because the gem is a default
372
+ # gem, which is exempt from the check on the host while its packed
373
+ # regular-spec copy is not. Recreate the marker where RubyGems expects
374
+ # it at runtime.
375
+ if !Gem.win_platform? && spec.extensions.any?
376
+ api_version = Gem.respond_to?(:extension_api_version) ? Gem.extension_api_version : Gem.ruby_api_version
377
+ packed_ext_dir = Pathname("extensions") / Gem::Platform.local.to_s / api_version / spec.full_name
378
+ host_marker = Pathname(spec.gem_build_complete_path)
379
+ if default_spec || (File.exist?(host_marker) && !host_marker.subpath?(base_dir))
380
+ builder.touch(packed_base / packed_ext_dir / "gem.build_complete")
381
+ end
382
+ end
383
+
287
384
  # Determine which set of files to include for this particular gem
288
385
  include = GemSpecQueryable.gem_inclusion_set(spec.name, @option.gem_options)
289
386
  say "Detected gem #{spec.full_name} (#{include.join(", ")})"
@@ -292,6 +389,9 @@ module Ocran
292
389
 
293
390
  verbose "\tgem_dir: #{spec.gem_dir}"
294
391
  verbose "\tgem_dir exists: #{File.directory?(spec.gem_dir)}"
392
+ unless File.directory?(spec.gem_dir)
393
+ verbose "\tGem directory does not exist (default gem?); packing loaded files via load path instead"
394
+ end
295
395
  loaded_matches = include.include?(:loaded) ? features.select { |f| f.subpath?(spec.gem_dir) } : []
296
396
  verbose "\t:loaded candidates in features: #{loaded_matches.size}"
297
397
  loaded_matches.each { |f| verbose "\t loaded: #{f}" }
@@ -313,11 +413,72 @@ module Ocran
313
413
  end
314
414
  end
315
415
 
416
+ # When a distro builds native extensions outside the gem's base_dir
417
+ # and loads them from there (e.g. Fedora's /usr/lib64/gems/ruby), the
418
+ # loaded .so would be packed at a mirror path that is never on the
419
+ # runtime load path. Pack such loaded extension files ONLY into the
420
+ # extension directory RubyGems computes at runtime, which gem
421
+ # activation puts on the load path. Appended to actual_files after
422
+ # the generic copy above so they are claimed as gem files (and thus
423
+ # excluded from the plain feature packing, where a bare .so on
424
+ # RUBYLIB would shadow the gem) without also being mirrored at their
425
+ # host location. The extension dir and the load location may be
426
+ # aliased via symlinks in either direction, so match features against
427
+ # the extension dir by realpath both ways.
428
+ if !Gem.win_platform? && spec.extensions.any?
429
+ host_ext_dir = Pathname(spec.extension_dir)
430
+ if host_ext_dir.directory? && !host_ext_dir.subpath?(base_dir)
431
+ alias_map = {}
432
+ host_ext_dir.find.select(&:file?).each do |entry|
433
+ real = begin
434
+ entry.realpath
435
+ rescue SystemCallError
436
+ next
437
+ end
438
+ alias_map[real] = entry
439
+ end
440
+ features.each do |feature|
441
+ real = begin
442
+ feature.realpath
443
+ rescue SystemCallError
444
+ feature
445
+ end
446
+ entry = if feature.subpath?(host_ext_dir)
447
+ feature
448
+ elsif real.subpath?(host_ext_dir)
449
+ real
450
+ else
451
+ alias_map[feature] || alias_map[real]
452
+ end
453
+ next unless entry
454
+
455
+ builder.cp(feature, packed_base / packed_ext_dir / entry.relative_path_from(host_ext_dir))
456
+ actual_files << feature
457
+ end
458
+ end
459
+ end
460
+
316
461
  actual_files
317
462
  end
318
463
  gem_files.uniq!
319
464
 
320
- features -= gem_files
465
+ # On some distros parts of a gem are reachable through symlinks at other
466
+ # locations (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the
467
+ # psych gem directory), so a feature and a packed gem file can denote
468
+ # the same file under different paths. Compare realpaths when removing
469
+ # gem files from the feature list; otherwise a stdlib-level duplicate
470
+ # would be packed as well and shadow the packed gem at runtime.
471
+ gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil }).to_set
472
+ features = features.reject do |feature|
473
+ next true if gem_file_set.include?(feature)
474
+
475
+ real = begin
476
+ feature.realpath
477
+ rescue SystemCallError
478
+ nil
479
+ end
480
+ real && gem_file_set.include?(real)
481
+ end
321
482
 
322
483
  # If requested, add all ruby standard libraries
323
484
  if @option.add_all_core?
@@ -500,13 +661,16 @@ module Ocran
500
661
  # By adding the extract-dir equivalents of rubylibdir, sitelibdir, etc. to
501
662
  # RUBYLIB, Ruby can find rubygems and the standard library in the packed tree.
502
663
  unless Gem.win_platform?
503
- core_lib_paths = all_core_dir
664
+ # Use the build Ruby's actual default load path in addition to the
665
+ # RbConfig directories: some distros compile in extra entries that
666
+ # RbConfig does not expose (e.g. Fedora's /usr/share/rubygems, where
667
+ # rubygems.rb lives outside rubylibdir).
668
+ default_load_paths = @pre_env.load_path.map { |dir| Pathname(@pre_env.expand_path(dir)) }
669
+ archdir = Pathname(RbConfig::CONFIG["archdir"])
670
+ core_lib_paths = (default_load_paths + all_core_dir + [archdir])
504
671
  .select { |dir| dir.subpath?(exec_prefix) }
505
672
  .map { |dir| dir.relative_path_from(exec_prefix) }
506
- archdir = Pathname(RbConfig::CONFIG["archdir"])
507
- if archdir.subpath?(exec_prefix)
508
- core_lib_paths << archdir.relative_path_from(exec_prefix)
509
- end
673
+ .uniq
510
674
  load_path = core_lib_paths + load_path
511
675
  end
512
676
 
@@ -515,15 +679,24 @@ module Ocran
515
679
 
516
680
  gem_paths = [GEMDIR]
517
681
  # Gems installed under the Ruby prefix (exec_prefix) have their specs and
518
- # extension dirs placed there via duplicate_to_exec_prefix. Include
519
- # Gem.default_dir (relative to exec_prefix) in GEM_PATH so RubyGems can
520
- # find and activate them at runtime. This is required on both Windows
521
- # (e.g. fxruby/fox16 whose fox16_c.so lives in extension_dir under the
522
- # Ruby prefix) and POSIX (e.g. error_highlight default gems).
523
- default_gem_dir = Pathname(Gem.default_dir)
524
- if default_gem_dir.subpath?(exec_prefix)
525
- gem_paths << default_gem_dir.relative_path_from(exec_prefix)
526
- end
682
+ # extension dirs placed there via duplicate_to_exec_prefix. Include every
683
+ # Gem.path entry located under exec_prefix (relative to it) in GEM_PATH so
684
+ # RubyGems can find and activate them at runtime. This is required on both
685
+ # Windows (e.g. fxruby/fox16 whose fox16_c.so lives in extension_dir under
686
+ # the Ruby prefix) and POSIX (e.g. error_highlight default gems). Distros
687
+ # can split gems over several such directories - e.g. Fedora uses
688
+ # /usr/share/gems for RPM-packaged (default) gems and /usr/local/share/gems
689
+ # for user-installed ones - so Gem.default_dir alone is not enough.
690
+ prefix_gem_dirs = (Gem.path.map { |dir| Pathname(dir) } + [Pathname(Gem.default_dir)])
691
+ .select { |dir| dir.subpath?(exec_prefix) }
692
+ .map { |dir| dir.relative_path_from(exec_prefix) }
693
+ .uniq
694
+ gem_paths += prefix_gem_dirs
695
+ # RubyGems probes the default gem dir for writability at startup and
696
+ # prints "Can't determine writability of default gem path" on stderr
697
+ # when the directory does not exist in the packed layout - so always
698
+ # create the packed prefix gem dirs, even when no specs landed there.
699
+ prefix_gem_dirs.each { |dir| builder.mkdir(dir) }
527
700
  builder.set_env_path("GEM_PATH", *gem_paths)
528
701
 
529
702
  # Add the opcode to launch the script
@@ -546,8 +719,13 @@ module Ocran
546
719
  title: @option.output_executable.basename.sub_ext("")
547
720
  )
548
721
 
722
+ # Record the launch events (exports + exec) so they can be replayed
723
+ # into the wrapper stub executable below.
724
+ require_relative "launcher_event_recorder"
725
+ recorder = LauncherEventRecorder.new(launcher_builder)
726
+
549
727
  require_relative "build_facade"
550
- builder = BuildFacade.new(iss_builder, launcher_builder)
728
+ builder = BuildFacade.new(iss_builder, recorder)
551
729
 
552
730
  if @option.icon_filename
553
731
  builder.cp(@option.icon_filename, File.basename(@option.icon_filename))
@@ -560,22 +738,65 @@ module Ocran
560
738
  verbose File.read(launcher_path)
561
739
  builder.cp(launcher_path, "launcher.bat")
562
740
 
563
- say "Build inno setup script file"
564
- iss_path = iss_builder.build
565
- verbose File.read(iss_path)
741
+ require "tmpdir"
742
+ Dir.mktmpdir("ocran") do |tmpdir|
743
+ if @option.wrapper_exe?
744
+ # Wrapper executable (pre-1.4/OCRA behavior): a small stub without
745
+ # embedded files that runs the installed application directly from
746
+ # {app}. It is installed next to the application files so that user
747
+ # Inno Setup scripts can reference it by the --output name, e.g. in
748
+ # [Run]/[UninstallRun] entries or for Windows service registration.
749
+ wrapper_path = Pathname(tmpdir) / @option.output_executable.basename
750
+ build_wrapper_exe(wrapper_path) { |stub| recorder.replay(stub) }
751
+ builder.cp(wrapper_path, wrapper_path.basename)
752
+ end
753
+
754
+ say "Build inno setup script file"
755
+ iss_path = iss_builder.build
756
+ verbose File.read(iss_path)
566
757
 
567
- say "Running Inno Setup Command-Line compiler (ISCC)"
568
- iss_builder.compile(verbose: @option.verbose?)
758
+ say "Running Inno Setup Command-Line compiler (ISCC)"
759
+ iss_builder.compile(verbose: @option.verbose?)
760
+ end
569
761
 
570
762
  say "Finished building installer file"
571
763
  end
572
764
 
765
+ # Builds the small RUN_IN_EXE_DIR wrapper stub that starts the deployed
766
+ # application directly from the directory the wrapper resides in.
767
+ def build_wrapper_exe(wrapper_path)
768
+ require_relative "stub_builder"
769
+ say "Build wrapper executable #{wrapper_path.basename}"
770
+ StubBuilder.new(wrapper_path,
771
+ chdir_before: @option.chdir_before?,
772
+ debug_mode: @option.enable_debug_mode?,
773
+ gui_mode: @option.windowed?,
774
+ icon_path: @option.icon_filename,
775
+ run_in_exe_dir: true) do |stub|
776
+ yield(stub)
777
+ end
778
+ end
779
+
573
780
  def build_output_dir(path)
574
781
  require_relative "dir_builder"
575
782
 
576
783
  path = Pathname(path)
577
784
  say "Building directory #{path}"
578
- DirBuilder.new(path, &to_proc)
785
+ builder = DirBuilder.new(path, &to_proc)
786
+
787
+ if @option.wrapper_exe?
788
+ # Same wrapper as in Inno Setup builds: a doubleclickable executable
789
+ # next to the launch script, usable e.g. for Windows service
790
+ # registration from an unpacked zip. Disable with --no-wrapper-exe.
791
+ build_wrapper_exe(path / @option.output_executable.basename) do |stub|
792
+ builder.env.each { |name, value| stub.export(name, value) }
793
+ if builder.exec_args
794
+ image, script, argv = builder.exec_args
795
+ stub.exec(image, script, *argv)
796
+ end
797
+ end
798
+ end
799
+
579
800
  say "Finished building directory #{path}"
580
801
  end
581
802
 
@@ -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
 
@@ -95,8 +96,16 @@ module Ocran
95
96
  # icon_path:
96
97
  # Specifies the path to the icon file to be embedded in the stub's resources.
97
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
+ #
98
106
  def initialize(path, chdir_before: nil, debug_extract: nil, debug_mode: nil,
99
- 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)
100
109
  @dirs = FilePathSet.new
101
110
  @files = FilePathSet.new
102
111
  @data_size = 0
@@ -129,7 +138,7 @@ module Ocran
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.3"
4
+ VERSION = "1.4.4"
5
5
  end
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
 
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.4
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:
@@ -73,6 +59,7 @@ files:
73
59
  - lib/ocran/host_config_helper.rb
74
60
  - lib/ocran/inno_setup_script_builder.rb
75
61
  - lib/ocran/launcher_batch_builder.rb
62
+ - lib/ocran/launcher_event_recorder.rb
76
63
  - lib/ocran/library_detector.rb
77
64
  - lib/ocran/library_detector_posix.rb
78
65
  - lib/ocran/option.rb
@@ -110,7 +97,7 @@ licenses:
110
97
  metadata:
111
98
  homepage_uri: https://github.com/largo/ocran
112
99
  source_code_uri: https://github.com/largo/ocran
113
- changelog_uri: https://github.com/largo/ocran/CHANGELOG.txt
100
+ changelog_uri: https://github.com/largo/ocran/blob/master/CHANGELOG.txt
114
101
  rdoc_options: []
115
102
  require_paths:
116
103
  - lib
@@ -125,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
112
  - !ruby/object:Gem::Version
126
113
  version: '0'
127
114
  requirements: []
128
- rubygems_version: 4.0.6
115
+ rubygems_version: 4.0.16
129
116
  specification_version: 4
130
117
  summary: OCRAN (One-Click Ruby Application Next) packages Ruby applications for distribution
131
118
  on Windows, Linux, and macOS.