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 +4 -4
- data/CHANGELOG.txt +19 -0
- data/README.md +104 -15
- data/lib/ocran/build_facade.rb +2 -0
- data/lib/ocran/dir_builder.rb +3 -0
- data/lib/ocran/direction.rb +244 -23
- data/lib/ocran/gem_spec_queryable.rb +24 -3
- data/lib/ocran/inno_setup_script_builder.rb +15 -1
- data/lib/ocran/launcher_event_recorder.rb +29 -0
- data/lib/ocran/option.rb +7 -0
- data/lib/ocran/runner.rb +4 -2
- data/lib/ocran/stub_builder.rb +19 -4
- data/lib/ocran/version.rb +1 -1
- data/src/inst_dir.c +44 -2
- data/src/inst_dir.h +14 -0
- data/src/stub.c +19 -8
- data/src/system_utils.c +26 -0
- data/src/system_utils.h +14 -0
- data/src/system_utils_posix.c +6 -0
- data/src/unpack.c +4 -0
- data/src/unpack.h +9 -0
- metadata +11 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79aa33b2625758b6cce7b1175d45d73c5c60209b996a6d5ab03a4df55acbae28
|
|
4
|
+
data.tar.gz: 3d53ff8c8b7a31448080f105f33af4968dea8f9a8fd559aa92a9e87db31db0dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
1
|
+
# OCRAN
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
data/lib/ocran/build_facade.rb
CHANGED
data/lib/ocran/dir_builder.rb
CHANGED
|
@@ -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)
|
data/lib/ocran/direction.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
520
|
-
# find and activate them at runtime. This is required on both
|
|
521
|
-
# (e.g. fxruby/fox16 whose fox16_c.so lives in extension_dir under
|
|
522
|
-
# Ruby prefix) and POSIX (e.g. error_highlight default gems).
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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,
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
|
|
134
|
+
# Find the selected files. Default gems (e.g. fiddle, singleton on
|
|
135
|
+
# Homebrew or distro-packaged Ruby) may have a gemspec without a
|
|
136
|
+
# materialized gem directory - their files live in Ruby's stdlib and
|
|
137
|
+
# are packed via the load path instead.
|
|
138
|
+
def gem_root_files = gem_root.directory? ? gem_root.find.select(&:file?) : []
|
|
136
139
|
|
|
137
140
|
def script_files
|
|
138
141
|
gem_root_files.select { |path| path.extname =~ GEM_SCRIPT_RE }
|
|
@@ -154,7 +157,25 @@ module Ocran
|
|
|
154
157
|
when :spec
|
|
155
158
|
files.map { |file| Pathname(file) }
|
|
156
159
|
when :loaded
|
|
157
|
-
|
|
160
|
+
# Some distros expose gem files under additional paths via symlinks
|
|
161
|
+
# (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the psych gem
|
|
162
|
+
# directory) and $LOADED_FEATURES records the symlink path. Resolve
|
|
163
|
+
# each feature to its realpath so such files count as gem files and
|
|
164
|
+
# are packed inside the gem directory. Packing them at the symlink
|
|
165
|
+
# location instead would break require_relative, which resolves
|
|
166
|
+
# against the realpath on the build host but not in the packed app.
|
|
167
|
+
features_from_gems.filter_map do |feature|
|
|
168
|
+
if feature.subpath?(gem_dir)
|
|
169
|
+
feature
|
|
170
|
+
else
|
|
171
|
+
real = begin
|
|
172
|
+
feature.realpath
|
|
173
|
+
rescue SystemCallError
|
|
174
|
+
next
|
|
175
|
+
end
|
|
176
|
+
real if real.subpath?(gem_dir)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
158
179
|
when :files
|
|
159
180
|
resource_files
|
|
160
181
|
when :extras
|
|
@@ -14,7 +14,14 @@ module Ocran
|
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
16
|
def compile(iss_filename, quiet: false)
|
|
17
|
-
|
|
17
|
+
# "where" and ">NUL" only exist on Windows; use "command -v" on POSIX
|
|
18
|
+
# (e.g. when testing the pipeline with a fake ISCC on Linux/macOS).
|
|
19
|
+
iscc_found = if Gem.win_platform?
|
|
20
|
+
true # ISCC is invoked directly; failure is reported below
|
|
21
|
+
else
|
|
22
|
+
system("command -v #{quote_and_escape(ISCC_CMD)} > /dev/null 2>&1")
|
|
23
|
+
end
|
|
24
|
+
unless iscc_found
|
|
18
25
|
raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
|
|
19
26
|
end
|
|
20
27
|
|
|
@@ -79,6 +86,13 @@ module Ocran
|
|
|
79
86
|
@dirs.add?("/", target)
|
|
80
87
|
end
|
|
81
88
|
|
|
89
|
+
# Symbolic links cannot be expressed in an Inno Setup script. They only
|
|
90
|
+
# occur when building from POSIX hosts (e.g. libruby.so links); Windows
|
|
91
|
+
# installations do not need them, so they are skipped.
|
|
92
|
+
def symlink(_target, _link_name)
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
82
96
|
def cp(source, target)
|
|
83
97
|
unless File.exist?(source)
|
|
84
98
|
raise "The file does not exist (#{source})"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ocran
|
|
4
|
+
# Records launcher-directed build events (environment exports and the
|
|
5
|
+
# application exec command) while forwarding them to another launcher
|
|
6
|
+
# builder. The recorded events can later be replayed into a further
|
|
7
|
+
# builder — e.g. to produce a wrapper stub executable alongside the
|
|
8
|
+
# launcher batch file in Inno Setup builds.
|
|
9
|
+
class LauncherEventRecorder
|
|
10
|
+
def initialize(launcher)
|
|
11
|
+
@launcher = launcher
|
|
12
|
+
@events = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def export(name, value)
|
|
16
|
+
@events << [:export, name, value]
|
|
17
|
+
@launcher.export(name, value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exec(image, script, *argv)
|
|
21
|
+
@events << [:exec, image, script, *argv]
|
|
22
|
+
@launcher.exec(image, script, *argv)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replay(builder)
|
|
26
|
+
@events.each { |event, *args| builder.public_send(event, *args) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/ocran/option.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Ocran
|
|
|
37
37
|
:source_files => [],
|
|
38
38
|
:verbose? => false,
|
|
39
39
|
:warning? => true,
|
|
40
|
+
:wrapper_exe? => true,
|
|
40
41
|
}
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -90,6 +91,8 @@ Output options:
|
|
|
90
91
|
--output <file> Name the exe to generate. Defaults to ./<scriptname>.exe.
|
|
91
92
|
--output-dir <dir> Output all files to a directory with a launch script instead of an exe.
|
|
92
93
|
--output-zip <file> Output a zip archive containing all files and a launch script.
|
|
94
|
+
--no-wrapper-exe Do not add the wrapper executable to installer, directory or
|
|
95
|
+
zip output (a launch script is always included there).
|
|
93
96
|
--macosx-bundle Build a macOS .app bundle. Use --output to name it (default: <scriptname>.app).
|
|
94
97
|
--bundle-id <id> Bundle identifier for the macOS app bundle (default: com.example.<appname>).
|
|
95
98
|
--no-lzma Disable LZMA compression of the executable.
|
|
@@ -125,6 +128,8 @@ EOF
|
|
|
125
128
|
when "--output-zip"
|
|
126
129
|
path = argv.shift
|
|
127
130
|
@options[:output_zip] = Pathname.new(path).expand_path if path
|
|
131
|
+
when "--no-wrapper-exe"
|
|
132
|
+
@options[:wrapper_exe?] = false
|
|
128
133
|
when "--macosx-bundle"
|
|
129
134
|
@options[:macosx_bundle?] = true
|
|
130
135
|
when "--bundle-id"
|
|
@@ -301,6 +306,8 @@ EOF
|
|
|
301
306
|
|
|
302
307
|
def quiet? = @options[__method__]
|
|
303
308
|
|
|
309
|
+
def wrapper_exe? = @options[__method__]
|
|
310
|
+
|
|
304
311
|
def rubyopt = @options[__method__]
|
|
305
312
|
|
|
306
313
|
def run_script? = @options[__method__]
|
data/lib/ocran/runner.rb
CHANGED
|
@@ -94,10 +94,12 @@ module Ocran
|
|
|
94
94
|
direction = Direction.new(@post_env, @pre_env, @option)
|
|
95
95
|
|
|
96
96
|
if @option.use_inno_setup?
|
|
97
|
-
|
|
97
|
+
# Native on Windows; on POSIX allow it when an ISCC command is
|
|
98
|
+
# available (e.g. via Wine wrapper scripts or in tests).
|
|
99
|
+
if Gem.win_platform? || system("command -v ISCC > /dev/null 2>&1")
|
|
98
100
|
direction.build_inno_setup_installer
|
|
99
101
|
else
|
|
100
|
-
raise "Inno Setup is only supported on Windows"
|
|
102
|
+
raise "Inno Setup is only supported on Windows (no ISCC command found in PATH)"
|
|
101
103
|
end
|
|
102
104
|
elsif @option.macosx_bundle
|
|
103
105
|
direction.build_macosx_bundle(@option.macosx_bundle)
|
data/lib/ocran/stub_builder.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Ocran
|
|
|
20
20
|
AUTO_CLEAN_INST_DIR = 0x04
|
|
21
21
|
CHDIR_BEFORE_SCRIPT = 0x08
|
|
22
22
|
DATA_COMPRESSED = 0x10
|
|
23
|
+
RUN_IN_EXE_DIR = 0x20
|
|
23
24
|
|
|
24
25
|
WINDOWS = Gem.win_platform?
|
|
25
26
|
|
|
@@ -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
data/src/inst_dir.c
CHANGED
|
@@ -98,8 +98,50 @@ const char *CreateInstDir(bool is_extract_to_exe_dir)
|
|
|
98
98
|
return NULL;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/* Normalize 8.3 short names (e.g. a TEMP under C:\Users\RUNNER~1) so all
|
|
102
|
+
paths derived from the extraction dir use one consistent spelling. */
|
|
103
|
+
char *long_dir = ToLongPath(inst_dir);
|
|
104
|
+
free(inst_dir);
|
|
105
|
+
if (!long_dir) {
|
|
106
|
+
return NULL;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
InstDir = long_dir;
|
|
110
|
+
return InstDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sets the installation directory to the executable's own directory
|
|
114
|
+
// (installer/wrapper mode, RUN_IN_EXE_DIR). No directory is created and
|
|
115
|
+
// the directory must never be deleted by the stub.
|
|
116
|
+
const char *SetInstDirToExeDir(void)
|
|
117
|
+
{
|
|
118
|
+
if (InstDir != NULL) {
|
|
119
|
+
APP_ERROR("Installation directory has already been set");
|
|
120
|
+
return NULL;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
char *image_path = GetImagePath();
|
|
124
|
+
if (!image_path) {
|
|
125
|
+
APP_ERROR("Failed to get executable name");
|
|
126
|
+
return NULL;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
char *image_dir = GetParentPath(image_path);
|
|
130
|
+
free(image_path);
|
|
131
|
+
if (!image_dir) {
|
|
132
|
+
APP_ERROR("Failed to obtain the directory path of the executable file");
|
|
133
|
+
return NULL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Normalize 8.3 short names for a consistent spelling (see CreateInstDir) */
|
|
137
|
+
char *long_dir = ToLongPath(image_dir);
|
|
138
|
+
free(image_dir);
|
|
139
|
+
if (!long_dir) {
|
|
140
|
+
return NULL;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
InstDir = long_dir;
|
|
144
|
+
return InstDir;
|
|
103
145
|
}
|
|
104
146
|
|
|
105
147
|
// Frees the allocated memory for the installation directory path.
|
data/src/inst_dir.h
CHANGED
|
@@ -20,6 +20,20 @@
|
|
|
20
20
|
*/
|
|
21
21
|
const char *CreateInstDir(bool is_extract_to_exe_dir);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @brief Sets the installation directory to the executable's own directory.
|
|
25
|
+
*
|
|
26
|
+
* Unlike CreateInstDir, no new directory is created: the stub runs the
|
|
27
|
+
* application directly from the directory the executable resides in
|
|
28
|
+
* (installer/wrapper mode, RUN_IN_EXE_DIR). The directory must never be
|
|
29
|
+
* deleted by the stub.
|
|
30
|
+
*
|
|
31
|
+
* @return
|
|
32
|
+
* A pointer to the directory path if successful, NULL if an error
|
|
33
|
+
* occurred. The returned path should not be freed by the caller.
|
|
34
|
+
*/
|
|
35
|
+
const char *SetInstDirToExeDir(void);
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* @brief Free the allocated installation directory path
|
|
25
39
|
* and reset the internal pointer to NULL.
|
data/src/stub.c
CHANGED
|
@@ -57,15 +57,24 @@ int main(int argc, char *argv[])
|
|
|
57
57
|
DEBUG("Ocran stub running in debug mode");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/* Create extraction directory
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/* Create extraction directory, or run in place next to the executable
|
|
61
|
+
(installer/wrapper mode, see RUN_IN_EXE_DIR) */
|
|
62
|
+
if (IsRunInExeDir(op_modes)) {
|
|
63
|
+
extract_dir = SetInstDirToExeDir();
|
|
64
|
+
if (!extract_dir) {
|
|
65
|
+
FATAL("Failed to resolve the executable directory");
|
|
66
|
+
goto cleanup;
|
|
67
|
+
}
|
|
68
|
+
DEBUG("Running in executable directory: %s", extract_dir);
|
|
69
|
+
} else {
|
|
70
|
+
extract_dir = CreateInstDir(IsExtractToExeDir(op_modes));
|
|
71
|
+
if (!extract_dir) {
|
|
72
|
+
FATAL("Failed to create extraction directory");
|
|
73
|
+
goto cleanup;
|
|
74
|
+
}
|
|
75
|
+
DEBUG("Created extraction directory: %s", extract_dir);
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
DEBUG("Created extraction directory: %s", extract_dir);
|
|
68
|
-
|
|
69
78
|
/* Unpacking process */
|
|
70
79
|
if (!ProcessImage(unpack_ctx)) {
|
|
71
80
|
FATAL("Failed to unpack image due to invalid or corrupted data");
|
|
@@ -120,7 +129,9 @@ cleanup:
|
|
|
120
129
|
/*
|
|
121
130
|
If AUTO_CLEAN_INST_DIR is set, delete the extraction directory.
|
|
122
131
|
*/
|
|
123
|
-
|
|
132
|
+
/* Never delete in RUN_IN_EXE_DIR mode: the "installation directory"
|
|
133
|
+
is the real application directory, not a temporary extraction dir. */
|
|
134
|
+
if (IsAutoCleanInstDir(op_modes) && !IsRunInExeDir(op_modes)) {
|
|
124
135
|
DEBUG("Deleting extraction directory: %s", extract_dir);
|
|
125
136
|
if (!DeleteInstDir()) {
|
|
126
137
|
DEBUG("Failed to delete extraction directory");
|
data/src/system_utils.c
CHANGED
|
@@ -96,6 +96,32 @@ char *JoinPath(const char *p1, const char *p2)
|
|
|
96
96
|
return joined_path;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
char *ToLongPath(const char *path)
|
|
100
|
+
{
|
|
101
|
+
if (!path) {
|
|
102
|
+
return NULL;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
DWORD required = GetLongPathName(path, NULL, 0);
|
|
106
|
+
if (required == 0) {
|
|
107
|
+
return _strdup(path); /* e.g. path does not exist - keep as is */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
char *buf = malloc(required);
|
|
111
|
+
if (!buf) {
|
|
112
|
+
APP_ERROR("Memory allocation failed for long path");
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
DWORD written = GetLongPathName(path, buf, required);
|
|
117
|
+
if (written == 0 || written >= required) {
|
|
118
|
+
free(buf);
|
|
119
|
+
return _strdup(path);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return buf;
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
char *GetParentPath(const char *path)
|
|
100
126
|
{
|
|
101
127
|
if (!path) {
|
data/src/system_utils.h
CHANGED
|
@@ -102,6 +102,20 @@ char *GetImagePath(void);
|
|
|
102
102
|
*/
|
|
103
103
|
char *GetTempDirectoryPath(void);
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @brief Normalizes a path to its long form.
|
|
107
|
+
*
|
|
108
|
+
* On Windows, converts 8.3 short names (e.g. "C:\\Users\\RUNNER~1") to the
|
|
109
|
+
* full long path via GetLongPathName. Two spellings of the same directory
|
|
110
|
+
* would otherwise defeat Ruby's $LOADED_FEATURES deduplication, causing
|
|
111
|
+
* files to be loaded twice ("already initialized constant" warnings).
|
|
112
|
+
* On POSIX this simply returns a copy of the given path.
|
|
113
|
+
*
|
|
114
|
+
* @return A newly allocated string (caller frees), or NULL on allocation
|
|
115
|
+
* failure. Falls back to a copy of the input if conversion fails.
|
|
116
|
+
*/
|
|
117
|
+
char *ToLongPath(const char *path);
|
|
118
|
+
|
|
105
119
|
/**
|
|
106
120
|
* @brief Writes the contents of a buffer to the specified file path.
|
|
107
121
|
* Creates any missing parent directories and overwrites existing files.
|
data/src/system_utils_posix.c
CHANGED
|
@@ -498,3 +498,9 @@ bool CreateAndWaitForProcess(const char *app_name, char *argv[], int *exit_code)
|
|
|
498
498
|
|
|
499
499
|
return true;
|
|
500
500
|
}
|
|
501
|
+
|
|
502
|
+
/* POSIX has no short-path aliases - return a plain copy. */
|
|
503
|
+
char *ToLongPath(const char *path)
|
|
504
|
+
{
|
|
505
|
+
return path ? strdup(path) : NULL;
|
|
506
|
+
}
|
data/src/unpack.c
CHANGED
|
@@ -536,6 +536,10 @@ bool IsDataCompressed(OperationModes modes) {
|
|
|
536
536
|
return IsMode(modes, DATA_COMPRESSED);
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
bool IsRunInExeDir(OperationModes modes) {
|
|
540
|
+
return IsMode(modes, RUN_IN_EXE_DIR);
|
|
541
|
+
}
|
|
542
|
+
|
|
539
543
|
bool ProcessImage(const UnpackContext *context)
|
|
540
544
|
{
|
|
541
545
|
if (!context) {
|
data/src/unpack.h
CHANGED
|
@@ -66,6 +66,14 @@ typedef enum {
|
|
|
66
66
|
* required before using the data.
|
|
67
67
|
*/
|
|
68
68
|
DATA_COMPRESSED = 0x10,
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Runs the application directly from the executable's own directory
|
|
72
|
+
* instead of creating an extraction directory. Used by installer builds
|
|
73
|
+
* (e.g. Inno Setup) where the application files are installed next to
|
|
74
|
+
* the stub executable. Restores the pre-1.4 (OCRA) wrapper behavior.
|
|
75
|
+
*/
|
|
76
|
+
RUN_IN_EXE_DIR = 0x20,
|
|
69
77
|
} OperationModes;
|
|
70
78
|
|
|
71
79
|
bool IsDebugMode(OperationModes modes);
|
|
@@ -73,6 +81,7 @@ bool IsExtractToExeDir(OperationModes modes);
|
|
|
73
81
|
bool IsAutoCleanInstDir(OperationModes modes);
|
|
74
82
|
bool IsChdirBeforeScript(OperationModes modes);
|
|
75
83
|
bool IsDataCompressed(OperationModes modes);
|
|
84
|
+
bool IsRunInExeDir(OperationModes modes);
|
|
76
85
|
|
|
77
86
|
typedef struct UnpackContext UnpackContext;
|
|
78
87
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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.
|