ocran 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff520e4a8036bbcec4c8a1e56c96c89297cf43a88dd868d77ae5f4aadac7865c
4
- data.tar.gz: 7735500efac55f60da13cf8c854bb6f80967b091b8baad310889073c7b8093eb
3
+ metadata.gz: 79aa33b2625758b6cce7b1175d45d73c5c60209b996a6d5ab03a4df55acbae28
4
+ data.tar.gz: 3d53ff8c8b7a31448080f105f33af4968dea8f9a8fd559aa92a9e87db31db0dd
5
5
  SHA512:
6
- metadata.gz: 7f360b8ea6dd7f87887a9afaa31e1053d0208588f91e24f6ccb224e6e17afe5ca4f2c7558b99907caaea540097b62c14aa3e2fd0cd89ada4615b54da316c883f
7
- data.tar.gz: 5586f5255f2d966c12a23bbe934d1743b0b2f12158307dee2c94dbaa7b3731a08c31f019a378740280da2895d6a216c750da47b84f5fe30143bfc3d5d755bafe
6
+ metadata.gz: e9975e649595c77397e49649544fad56b209917a55f7ea6d7ff7acf748a14608165013fbe1e43908946b0cba1badbae22da7b73aac4ba2661fb092162d333386
7
+ data.tar.gz: 67007f4112f91a8bb6a01564a8b5b4b54edd2b852a629f78d1bc12bd9062e3a71771fe0a802bc9f3133560cd0aa7da18fa912f0eb0f2f8337288ea226be308b2
data/CHANGELOG.txt CHANGED
@@ -1,3 +1,29 @@
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
+
20
+ === 1.4.3
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.
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.
23
+ - Add FXRuby (fox16) test: verify that native extension DLLs are bundled correctly so the packaged executable does not raise a LoadError at runtime.
24
+ - CI: start a virtual display (xpra + Xorg dummy driver with GLX) on Linux before running tests so FXRuby's X11 requirement is satisfied.
25
+ - CI: install XQuartz on macOS runners and add it to the portability test job so FXRuby can connect to a display on both test and portability jobs.
26
+
1
27
  === 1.4.2
2
28
  - Auto-bundle OpenSSL and all its transitive dependencies (openssl.rb, digest.so, etc.) when net/http is loaded but no HTTPS request was made during the dependency scan. OpenSSL is now required inside the OCRAN build process so every file it pulls in appears in the bundled executable.
3
29
  - Add companion DLL scan on windows: when a native extension (.so) within the Ruby installation is loaded, all DLLs in the same directory are proactively included. This ensures libssl-3-x64.dll, libcrypto-3-x64.dll, libwinpthread-1.dll, and libyaml-0-2.dll are bundled even when no HTTPS connection is made during the build scan.
data/README.md CHANGED
@@ -1,12 +1,6 @@
1
- # ocran
1
+ # OCRAN
2
2
 
3
- home :: https://github.com/largo/ocran/
4
-
5
- issues :: http://github.com/largo/ocran/issues
6
-
7
- ## Description
8
-
9
- 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
10
4
  distribution. It bundles your script, the Ruby interpreter, gems, and native
11
5
  libraries into a single self-contained artifact that runs without requiring
12
6
  Ruby to be installed on the target machine.
@@ -21,20 +15,24 @@ OCRAN supports four output formats, all cross-platform:
21
15
  OCRAN is a fork of [OCRA](https://github.com/larsch/ocra) maintained for
22
16
  Ruby 3.2+ compatibility.
23
17
 
18
+ If you run into errors while using OCRAN, please check the [OCRAN issues](https://github.com/largo/ocran/issues) first.
19
+
24
20
  ## Recommended usage
25
21
 
26
- The most common use-case is shipping a program to Windows servers or users
27
- who do not have Ruby installed. By default, each time the `.exe` is opened it
22
+ The most common use-case is shipping a program to users running Windows / Linux / macOS
23
+ who do not have Ruby installed. By default, each time the `.exe` / executable is opened it
28
24
  extracts the Ruby interpreter and your code to a temporary directory and runs
29
25
  them from there.
30
26
 
31
- Because extraction takes time on each launch, consider using the Inno Setup
27
+ Because extraction takes time on each launch, use `--output-dir` or
28
+ `--output-zip` to produce a portable directory/archive that runs with the
29
+ bundled Ruby on Linux, macOS, or Windows.
30
+ If using Windows you can use the Inno Setup
32
31
  option (`--innosetup`) to produce a proper installer that extracts once to a
33
32
  permanent directory.
34
33
 
35
- For cross-platform packaging or CI artifacts, use `--output-dir` or
36
- `--output-zip` to produce a portable directory/archive that runs with the
37
- bundled Ruby on Linux, macOS, or Windows.
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
 
@@ -74,12 +72,12 @@ https://github.com/largo/ocran/releases/.
74
72
 
75
73
  ## Synopsis
76
74
 
77
- ### Building a Windows executable:
75
+ ### Building an executable:
78
76
 
79
77
  ocran script.rb
80
78
 
81
79
  Packages `script.rb`, the Ruby interpreter, and all dependencies (gems and
82
- DLLs) into `script.exe`.
80
+ DLLs) into `script.exe` or `script` on Linux or macOS.
83
81
 
84
82
  ### Building a portable directory (Linux / macOS / Windows):
85
83
 
@@ -238,12 +236,98 @@ 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
 
245
328
  * Ruby 3.2+
246
- * For building Windows `.exe`: Windows with RubyInstaller DevKit (mingw-w64), or Wine on Linux/macOS
329
+ * For building Windows `.exe`: Windows with [RubyInstaller DevKit](https://rubyinstaller.org/downloads/) (mingw-w64), or Wine on Linux/macOS
330
+ * For building Linux and MacOS binaries: the respective build tools
247
331
  * For `--output-dir` / `--output-zip`: any platform with Ruby 3.2+
248
332
  * For `--output-zip` on Linux/macOS: the `zip` command must be available
249
333
  * For `--output-zip` on Windows: PowerShell (included in Windows 8+)
@@ -319,8 +403,12 @@ If your script uses `Kernel#autoload`, OCRAN will attempt to load those
319
403
  constants so they are included in the output. Missing modules are ignored
320
404
  with a warning.
321
405
 
322
- Dynamic link libraries (.dll files, for example WxWidgets, or other
323
- 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`.
324
412
 
325
413
  ### Including libraries non-automatically
326
414
 
@@ -526,12 +614,19 @@ file:
526
614
  end
527
615
  end
528
616
 
617
+ ## See elsewhere
618
+
619
+ - [State of Ruby Packagers](https://gist.github.com/YOU54F/3775e66e6090e0371c11601e6b75c305)
620
+ - [Traveling Ruby](https://github.com/trubygems/traveling-ruby)
621
+
529
622
  ## Credits
530
623
 
531
- 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)
532
628
 
533
- Kevin Walzer of codebykevin, Maxim Samsonov for ocra2, John Mair for
534
- 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.
535
630
 
536
631
  Igor Pavlov for the LZMA compressor and decompressor (Public Domain).
537
632
 
@@ -545,7 +640,7 @@ http://ruby.morphball.net/vit-ruby-ico_en.html).
545
640
  (The MIT License)
546
641
 
547
642
  Copyright (c) 2009-2020 Lars Christensen
548
- Copyright (c) 2020-2025 The OCRAN Committers Team
643
+ Copyright (c) 2020-2026 The OCRAN Committers Team
549
644
 
550
645
  Permission is hereby granted, free of charge, to any person obtaining
551
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
 
@@ -514,15 +678,25 @@ module Ocran
514
678
  builder.set_env_path("GEM_HOME", GEMDIR)
515
679
 
516
680
  gem_paths = [GEMDIR]
517
- # On POSIX, default gems (e.g. error_highlight) are stored under the Ruby
518
- # installation's gem dir (Gem.default_dir), not in GEMDIR. Include it in
519
- # GEM_PATH so RubyGems can find and activate them in the extracted tree.
520
- unless Gem.win_platform?
521
- default_gem_dir = Pathname(Gem.default_dir)
522
- if default_gem_dir.subpath?(exec_prefix)
523
- gem_paths << default_gem_dir.relative_path_from(exec_prefix)
524
- end
525
- end
681
+ # Gems installed under the Ruby prefix (exec_prefix) have their specs and
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) }
526
700
  builder.set_env_path("GEM_PATH", *gem_paths)
527
701
 
528
702
  # Add the opcode to launch the script
@@ -545,8 +719,13 @@ module Ocran
545
719
  title: @option.output_executable.basename.sub_ext("")
546
720
  )
547
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
+
548
727
  require_relative "build_facade"
549
- builder = BuildFacade.new(iss_builder, launcher_builder)
728
+ builder = BuildFacade.new(iss_builder, recorder)
550
729
 
551
730
  if @option.icon_filename
552
731
  builder.cp(@option.icon_filename, File.basename(@option.icon_filename))
@@ -559,22 +738,65 @@ module Ocran
559
738
  verbose File.read(launcher_path)
560
739
  builder.cp(launcher_path, "launcher.bat")
561
740
 
562
- say "Build inno setup script file"
563
- iss_path = iss_builder.build
564
- 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
565
753
 
566
- say "Running Inno Setup Command-Line compiler (ISCC)"
567
- iss_builder.compile(verbose: @option.verbose?)
754
+ say "Build inno setup script file"
755
+ iss_path = iss_builder.build
756
+ verbose File.read(iss_path)
757
+
758
+ say "Running Inno Setup Command-Line compiler (ISCC)"
759
+ iss_builder.compile(verbose: @option.verbose?)
760
+ end
568
761
 
569
762
  say "Finished building installer file"
570
763
  end
571
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
+
572
780
  def build_output_dir(path)
573
781
  require_relative "dir_builder"
574
782
 
575
783
  path = Pathname(path)
576
784
  say "Building directory #{path}"
577
- 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
+
578
800
  say "Finished building directory #{path}"
579
801
  end
580
802