ocran 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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,22 @@ 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
+ If you want **one** binary that runs on all three systems and starts
35
+ without extracting anything, package a cosmopolitan Ruby with
36
+ `--cosmo-ruby <ruby.com>` (experimental, see
37
+ [Experimental options](#experimental-options)). Grab the interpreter from
38
+ [CosmoRuby releases](https://github.com/Largo/cosmoruby/releases/latest)
39
+ and point OCRAN at it:
40
+
41
+ ```
42
+ ocran app.rb --cosmo-ruby ./ruby.com
43
+ ```
44
+
45
+ It needs no compiler and no installer, and the same file runs on Linux,
46
+ macOS and Windows, on x86-64 and ARM.
47
+
48
+ You can easily generate binaries for the supported Operating Systems with GitHub
49
+ Actions — see [Building for multiple platforms with GitHub Actions](#building-for-multiple-platforms-with-github-actions).
38
50
 
39
51
  ## Features
40
52
 
@@ -168,11 +180,169 @@ Fine-tuning flags:
168
180
  * `--windows`: Force a Windows GUI application (uses `rubyw.exe`). (Windows only)
169
181
  * `--console`: Force a console application (uses `ruby.exe`). (Windows only)
170
182
  * `--chdir-first`: Change working directory to the app's extraction directory before the script starts.
183
+ * `--chdir-exe-dir`: Change working directory to the directory containing the executable before the script starts. Use this when your app reads or writes files that live next to the `.exe` using relative paths. Cannot be combined with `--chdir-first`.
171
184
  * `--icon <ico>`: Replace the default icon with a custom `.ico` file.
172
185
  * `--rubyopt <str>`: Set `RUBYOPT` when the executable runs.
173
186
  * `--debug`: Enable verbose output when the generated executable runs.
174
187
  * `--debug-extract`: Unpack to a local directory and do not delete after execution (useful for troubleshooting).
175
188
 
189
+ #### Experimental options:
190
+
191
+ * `--cosmo-ruby <ruby.com>`: Package a cosmopolitan-built Ruby (an APE)
192
+ as the bundled interpreter instead of the host Ruby. The produced
193
+ `.com` contains no host-native code at all and runs on Linux, Windows
194
+ and macOS, on x86-64 and ARM:
195
+
196
+ ```
197
+ ocran app.rb --cosmo-ruby /path/to/ruby.com
198
+ ```
199
+
200
+ **Where to get `ruby.com`.** Download a release from
201
+ [CosmoRuby](https://github.com/Largo/cosmoruby/releases/latest) — one
202
+ file, no installation:
203
+
204
+ ```
205
+ curl -L -o ruby.com https://github.com/Largo/cosmoruby/releases/latest/download/ruby.com
206
+ chmod +x ruby.com
207
+ ocran app.rb --cosmo-ruby ./ruby.com
208
+ ```
209
+
210
+ Any self-contained cosmopolitan Ruby works, but CosmoRuby is what this
211
+ option is developed and tested against. Releases from `v4.0.6-cosmo3`
212
+ onwards also support the ZIP packaging mode described below, which
213
+ needs no C compiler at all; earlier releases fall back to building a
214
+ launcher stub, which needs `cosmocc`. Building your own is documented
215
+ in [CosmoRuby's BUILDING.md](https://github.com/Largo/cosmoruby/blob/main/BUILDING.md).
216
+
217
+ This one option is the whole command line. The payload APE must be
218
+ fully self-contained, i.e. carry its standard library in its embedded
219
+ ZIP store (`/zip/lib/ruby/...`) — upstream cosmopolitan Ruby builds
220
+ do.
221
+
222
+ **Two ways of producing the executable.** OCRAN picks the better one
223
+ automatically, from the interpreter you give it:
224
+
225
+ | | ZIP packaging (default when supported) | APE launcher stub (fallback) |
226
+ |---|---|---|
227
+ | How | the executable **is** the interpreter, with your application injected into its ZIP store | an APE stub carries the interpreter and your application as a payload |
228
+ | Compiler | none | `cosmocc` (located automatically) |
229
+ | At every start | nothing is unpacked | ~21 MB unpacked into a temp directory |
230
+ | Startup (see below) | 0.23 s Linux / 0.49 s Windows | 0.79 s Linux / 1.5 s Windows |
231
+ | Temp directory | never created | created per run, leaked if the process is killed |
232
+ | Size | interpreter + application | LZMA-compressed, roughly half |
233
+ | `__dir__` of your script | `/zip/ocran/src` (inside the executable) | the temp directory |
234
+
235
+ ZIP packaging is used when the given interpreter runs an embedded
236
+ `/zip/main.rb` on startup (CosmoRuby builds do; OCRAN detects it by
237
+ inspecting the binary). Otherwise OCRAN falls back to the launcher
238
+ stub and locates a `cosmocc` toolchain for it (see `--cosmo` below).
239
+ Passing `--cosmo <toolchain>` explicitly always selects the launcher
240
+ stub, as do `--output-dir`, `--output-zip`, `--innosetup` and
241
+ `--macosx-bundle`, which are not single binaries.
242
+
243
+ **What changes for your application under ZIP packaging** (nothing
244
+ below applies to the launcher stub):
245
+ * Your files are not on disk at run time. `$0`, `__FILE__` and
246
+ `__dir__` point into the executable's archive
247
+ (`/zip/ocran/src/...`), on every platform and with `/` separators.
248
+ Reading, `require`, `require_relative` and `Dir.glob` all work
249
+ there; **writing does not**, and the archive cannot be a working
250
+ directory (`Dir.chdir("/zip")` fails).
251
+ * Files that ship **next to the executable** are unaffected: resolve
252
+ them through `ENV["OCRAN_EXECUTABLE"]`, which is the full path of
253
+ the running `.com` exactly as before (issue #32). Do not use
254
+ `__dir__` for that — it never meant "next to the exe" in either
255
+ mode, but here it is obviously wrong instead of subtly wrong.
256
+ * `--chdir-first` changes into the directory containing the
257
+ executable, since the application directory does not exist on disk.
258
+ * The whole command line reaches `ARGV`, unchanged. The interpreter
259
+ claims none of it, so `app.com --version`, `app.com -v` and
260
+ `app.com -- x` behave exactly as they would for a natively compiled
261
+ program (`--` is an ordinary argument, not a separator). This needs
262
+ a cosmopolitan Ruby with the fix from 2026-08; older builds parsed
263
+ leading option-shaped arguments themselves and failed with
264
+ `invalid option --verbose`.
265
+ * `RUBYOPT` is applied as far as it can be from inside the process:
266
+ `-I` and `-r` are replayed, other flags are reported at build time
267
+ and dropped (the interpreter has already started).
268
+ * `--icon` and `--debug-extract` have no effect and are reported.
269
+ * The executable is not compressed (it has to stay runnable), so
270
+ `--no-lzma` makes no difference to its size; individual packed files
271
+ are deflated.
272
+ * Exit codes are your application's, exactly, on every platform
273
+ including Windows. (Cosmopolitan encodes a POSIX wait status into
274
+ the Windows process exit code, so `exit 3` used to arrive as 768;
275
+ both the interpreter and the launcher stub now report the plain
276
+ status.)
277
+
278
+ Notes that apply to both:
279
+ * The payload is run once on the build host (via `/bin/sh`, using the
280
+ APE's shell-script self-bootstrap) to validate it and to query its
281
+ version and embedded gem directory.
282
+ * Dependency detection still runs under the **host** Ruby. Standard
283
+ library requires resolve at runtime from the payload's embedded
284
+ stdlib (the host stdlib is not packed); a warning is printed when
285
+ host and payload Ruby versions differ.
286
+ * Pure-Ruby application files and pure-Ruby gems are packed as usual
287
+ and activated via `GEM_PATH`. Native gems cannot work (the payload
288
+ is a statically linked `x86_64-cosmo` binary that cannot `dlopen`):
289
+ gems that the payload provides itself (e.g. `json`, `psych`,
290
+ `sqlite3`) are simply not packed, so the payload's own copy serves;
291
+ any other native gem aborts the build. This covers gems built from
292
+ source (`spec.extensions`) as well as **precompiled platform gems**
293
+ (e.g. `sqlite3-2.9.5-x86_64-linux-gnu`), which declare no
294
+ extensions but ship a prebuilt `.so` in their `lib` directory.
295
+ * `--add-all-core` and encoding-support packing are no-ops (the
296
+ payload embeds its complete stdlib and encodings).
297
+ * Linux/macOS build hosts only, console applications only
298
+ (`--windows` is rejected — cosmocc has no GUI `stubw` equivalent).
299
+ The launcher-stub fallback additionally needs `make`. The default
300
+ output name uses the `.com` extension (APE convention), e.g.
301
+ `app.rb` → `app.com`; an explicit `--output` is used verbatim.
302
+
303
+ **Measured** with a 8-file CLI using four pure-Ruby gems (thor,
304
+ terminal-table, rainbow, unicode-display_width) and cosmopolitan Ruby
305
+ 4.0.6, average of 10 runs:
306
+
307
+ | Build | Size | Startup (Linux) | Startup (Windows 11) |
308
+ |---|---|---|---|
309
+ | ZIP packaging | 21.5 MB | 0.23 s | 0.49 s |
310
+ | Launcher stub, `--no-lzma` | 22.8 MB | 0.24 s | 1.09 s |
311
+ | Launcher stub, LZMA (default) | 11.3 MB | 0.79 s | 1.52 s |
312
+
313
+ The stub's cost is unpacking ~21 MB per run, which LZMA turns into
314
+ decompression time: on Linux with a warm page cache an uncompressed
315
+ stub build is nearly as fast, on Windows it is not. ZIP packaging
316
+ trades the smaller, compressed artifact for constant startup and no
317
+ disk writes at all.
318
+
319
+ * `--cosmo <path>` (alias `--cosmo-toolchain`): Name the
320
+ [Cosmopolitan Libc](https://github.com/jart/cosmopolitan) `cosmocc`
321
+ toolchain that builds the APE launcher stub from its C sources at
322
+ packaging time, overriding the automatic lookup. `<path>` is either
323
+ the `cosmocc` executable itself or the toolchain directory (one
324
+ containing `bin/cosmocc`, e.g. an unpacked
325
+ [cosmocc.zip](https://cosmo.zip/pub/cosmocc/cosmocc.zip)).
326
+ Notes:
327
+ * With `--cosmo-ruby`, naming a toolchain explicitly is also how you
328
+ ask for the launcher stub instead of ZIP packaging (for the
329
+ extraction semantics: a real on-disk application directory, and
330
+ `ARGV` untouched by the interpreter's option parser).
331
+ * When a toolchain is needed but not named, `--cosmo-ruby` looks for
332
+ one in this order: the `COSMOCC` environment variable (the `cosmocc` executable
333
+ or its install directory), `cosmocc` in `PATH`, and finally the
334
+ conventional install locations `~/.cosmocc/*/bin/cosmocc`,
335
+ `~/cosmocc/*/bin/cosmocc`, `/opt/cosmocc/*/bin/cosmocc`,
336
+ `/opt/cosmo/bin/cosmocc` and `/usr/local/cosmocc/bin/cosmocc`
337
+ (newest version first). If none is found the build stops with a
338
+ message naming all three mechanisms.
339
+ * Compiled stubs are cached in `~/.cache/ocran` (keyed on the
340
+ toolchain and stub sources), so only the first build compiles.
341
+ * Given **without** `--cosmo-ruby`, the packaged Ruby runtime is still
342
+ the host platform's Ruby — the APE property then applies to the
343
+ launcher stub, not to the bundled application. See
344
+ `docs/cosmocc-port-plan.md` for status.
345
+
176
346
  ### Compilation:
177
347
 
178
348
  * OCRAN runs your script (using `Kernel#load`) and builds the output when it exits.
@@ -182,8 +352,9 @@ Fine-tuning flags:
182
352
 
183
353
  ### Running your application:
184
354
 
185
- * The working directory is not changed by OCRAN unless you use `--chdir-first`. You must change to the installation or temporary
186
- directory yourself. See also below.
355
+ * The working directory is not changed by OCRAN unless you use `--chdir-first`
356
+ (extraction directory) or `--chdir-exe-dir` (directory containing the
357
+ executable). See "Working directory" below.
187
358
  * When a `.exe` is running, `OCRAN_EXECUTABLE` points to the `.exe` with its full path.
188
359
  * The temporary location of the script is available via `$0`.
189
360
  * OCRAN does not set up the include path. Add `$:.unshift File.dirname($0)` at the start of your script if you need to `require` additional files from the same directory as your main script.
@@ -238,7 +409,92 @@ OCRAN on the same platform (and architecture) as the intended target**:
238
409
  There is no support for building a Windows `.exe` from a Linux or macOS host,
239
410
  or vice versa. If you need builds for multiple platforms, run OCRAN in CI on
240
411
  each target platform separately (e.g., a Windows runner for `.exe` builds and
241
- a Linux runner for Linux builds).
412
+ a Linux runner for Linux builds). See
413
+ [Building for multiple platforms with GitHub Actions](#building-for-multiple-platforms-with-github-actions)
414
+ for a ready-made setup.
415
+
416
+ ### Linux binary portability (glibc)
417
+
418
+ On Linux, OCRAN bundles the shared libraries your application loaded during
419
+ the build (libyaml, libssl, libcrypt, libgmp, libz, ...) next to the bundled
420
+ Ruby, so the executable also runs on distributions where those libraries are
421
+ missing or have different sonames. It works with distro-packaged Ruby too
422
+ (e.g. `dnf install ruby` on Fedora), including its split gem layout and
423
+ `rubypick` wrapper.
424
+
425
+ One thing can never be bundled: **glibc itself** (and its dynamic loader).
426
+ Those always come from the target system, and glibc is only
427
+ backward-compatible. In practice:
428
+
429
+ * A Linux binary runs on any distribution whose glibc is **at least as new**
430
+ as the build machine's.
431
+ * Build on the **oldest** distribution you want to support. For example, a
432
+ binary built on the oldest available `ubuntu-*` GitHub runner runs on that
433
+ Ubuntu version and everything newer, while a binary built on a
434
+ cutting-edge distribution (e.g. `fedora:latest`) will not run on older
435
+ Debian/Ubuntu releases (`` version `GLIBC_2.4x' not found ``).
436
+
437
+ Use `--no-autodll` to disable shared library bundling.
438
+
439
+ ## Building for multiple platforms with GitHub Actions
440
+
441
+ Since OCRAN is not a cross-compiler, the recommended way to ship your app for
442
+ Windows, Linux, and macOS is a GitHub Actions matrix that runs OCRAN natively
443
+ on each platform. A minimal workflow:
444
+
445
+ ```yaml
446
+ name: Build binaries
447
+
448
+ on:
449
+ push:
450
+ tags: ['v*'] # build releases from version tags
451
+ workflow_dispatch: # allow manual runs
452
+
453
+ jobs:
454
+ build:
455
+ strategy:
456
+ fail-fast: false
457
+ matrix:
458
+ include:
459
+ - os: windows-latest # x86-64 Windows .exe
460
+ - os: ubuntu-22.04 # x86-64 Linux; oldest runner = widest glibc compatibility
461
+ - os: macos-14 # Apple Silicon macOS
462
+ - os: macos-15-intel # Intel macOS
463
+ runs-on: ${{ matrix.os }}
464
+ steps:
465
+ - uses: actions/checkout@v4
466
+
467
+ - uses: ruby/setup-ruby@v1
468
+ with:
469
+ ruby-version: '3.4'
470
+ bundler-cache: true # if your app has a Gemfile
471
+
472
+ - run: gem install ocran
473
+
474
+ # Run OCRAN on your entry script. Add "-- <args>" if your script needs
475
+ # arguments to exit cleanly during the dependency-detection run.
476
+ - run: ocran myapp.rb --output myapp
477
+ # produces myapp.exe on Windows, myapp on Linux/macOS
478
+
479
+ - uses: actions/upload-artifact@v4
480
+ with:
481
+ name: myapp-${{ matrix.os }}
482
+ path: myapp*
483
+ ```
484
+
485
+ Notes:
486
+
487
+ * Pick the **oldest** `ubuntu-*` runner GitHub offers for the Linux build so
488
+ the binary runs on as many distributions as possible (see
489
+ [Linux binary portability](#linux-binary-portability-glibc)).
490
+ * On macOS, each architecture needs its own build: `macos-14` (and newer
491
+ numbered ARM runners) produce Apple Silicon binaries, `macos-15-intel`
492
+ produces Intel binaries.
493
+ * Swap the `ocran` invocation for `--output-dir`, `--output-zip`,
494
+ `--macosx-bundle`, or `--innosetup` depending on how you want to ship
495
+ (see [Recommended usage](#recommended-usage)).
496
+ * For tag pushes you can attach the artifacts to a GitHub release with a
497
+ follow-up job (e.g. `softprops/action-gh-release`).
242
498
 
243
499
  ## Requirements
244
500
 
@@ -320,8 +576,12 @@ If your script uses `Kernel#autoload`, OCRAN will attempt to load those
320
576
  constants so they are included in the output. Missing modules are ignored
321
577
  with a warning.
322
578
 
323
- Dynamic link libraries (.dll files, for example WxWidgets, or other
324
- source files) will be detected and included by OCRAN.
579
+ Dynamic link libraries are detected and included by OCRAN: `.dll` files on
580
+ Windows (for example WxWidgets), and on Linux the non-glibc shared libraries
581
+ the process loaded (libyaml, libssl, libcrypt, ...), which are placed next to
582
+ the bundled Ruby with appropriate soname symlinks (see
583
+ [Linux binary portability](#linux-binary-portability-glibc)). Disable with
584
+ `--no-autodll`.
325
585
 
326
586
  ### Including libraries non-automatically
327
587
 
@@ -356,6 +616,55 @@ Four modes:
356
616
  If files are missing from the output, try `--gem-all=gemname` first, then
357
617
  `--gem-full=gemname`. Use `--gem-full` to include everything for all gems.
358
618
 
619
+ ### Using OCRAN under `bundle exec`
620
+
621
+ OCRAN loads your script inside its own process to find out what it depends
622
+ on, so the bundle OCRAN itself runs under is the bundle that dependency run
623
+ sees. That matters as soon as the two are not the same.
624
+
625
+ **Running `bundle exec ocran app.rb` requires `ocran` in the application's
626
+ Gemfile.** This is Bundler's rule, not OCRAN's: `bundle exec` refuses to run
627
+ a command the bundle does not contain ("ocran is not currently included in
628
+ the bundle"). Add `gem "ocran"` to the Gemfile, or install OCRAN with `gem
629
+ install ocran` and call `ocran` without `bundle exec`. Be aware that every
630
+ gem the Gemfile lists is packed into the application when you build with
631
+ `--gemfile`, OCRAN included.
632
+
633
+ **From the application's own directory it just works.** `bundle exec ocran
634
+ app.rb`, with or without `--gemfile Gemfile`, packages the application
635
+ against its own bundle - including gems declared with `path:` or `gemspec`,
636
+ which exist nowhere but that Gemfile.
637
+
638
+ **Packaging one project from inside another project's bundle needs
639
+ `--gemfile`.** If the bundle you are in is not the application's, pass
640
+ `--gemfile path/to/app/Gemfile`. It governs the dependency run as well as
641
+ the gem list, so a `require "bundler/setup"` in your script sets up the
642
+ application's bundle and not the one you happen to be standing in. Without
643
+ it the script is loaded under the wrong bundle and usually dies with a
644
+ `LoadError` for one of its own gems; OCRAN warns when it can see that this
645
+ is what you are doing.
646
+
647
+ Gems that the surrounding bundle merely activated are not packed. The
648
+ contents are decided by the application's own Gemfile and by what the
649
+ dependency run actually loads, so building a small application from inside a
650
+ large development bundle does not drag that bundle along.
651
+
652
+ **The packaged executable is independent of Bundler.** It carries its own
653
+ gems and its own Gemfile, and it clears the variables through which Bundler
654
+ hands a bundle to a child process (`BUNDLER_SETUP`, `BUNDLE_GEMFILE`,
655
+ `BUNDLE_LOCKFILE`, `RUBYOPT`), so it runs correctly even when started from a
656
+ `bundle exec` shell, from a Rakefile, or from another Ruby program.
657
+
658
+ **Exception: executables built with `--cosmo-ruby` cannot do that.** There
659
+ the cosmopolitan Ruby *is* the executable, so its RubyGems has already acted
660
+ on `BUNDLER_SETUP` before any packed code can run, and the application
661
+ aborts with `Bundler::GemNotFound` over gems of the bundle it was launched
662
+ from. Start such an executable outside a bundle, or clear the variables
663
+ first - `Bundler.with_original_env { system("./app.com") }` from Ruby, or
664
+ `env -u BUNDLER_SETUP -u BUNDLE_GEMFILE ./app.com` from a shell. Building
665
+ with `--cosmo-ruby` under `bundle exec` is fine; only launching the result
666
+ from inside a bundle is not.
667
+
359
668
  ### Code-signing a macOS app bundle
360
669
 
361
670
  After building with `--macosx-bundle`, sign the bundle with your Developer ID:
@@ -429,26 +738,51 @@ output, `OCRAN_EXECUTABLE` is set to the full path of the running executable:
429
738
 
430
739
  ### Working directory
431
740
 
432
- The OCRAN executable does not change the working directory when it starts. It only changes the working directory when you use
433
- `--chdir-first`.
434
-
435
- You should not assume that the current working directory when invoking
436
- an executable built with .exe is the location of the source script. It
437
- can be the directory where the executable is placed (when invoked
438
- through the Windows Explorer), the users' current working directory
439
- (when invoking from the Command Prompt), or even
440
- `C:\\WINDOWS\\SYSTEM32` when the executable is invoked through
441
- a file association.
442
-
443
- With `--chdir-first`, the working directory is always the common parent
444
- directory of your source files. Do not use this if your application takes
445
- filenames as command-line arguments.
741
+ By default the OCRAN executable does not change the working directory when it
742
+ starts. Two opt-in build options change this:
743
+
744
+ * `--chdir-first`: the working directory is always the common parent
745
+ directory of your source files inside the extraction directory. Do not use
746
+ this if your application takes filenames as command-line arguments.
747
+ * `--chdir-exe-dir`: the working directory is the directory that contains the
748
+ executable itself. Use this when your application reads or writes files that
749
+ are placed next to the `.exe` (config files, spreadsheets, output folders,
750
+ ...) using relative paths. Note that relative filenames passed as
751
+ command-line arguments will then also resolve against the executable's
752
+ directory instead of the invoker's current directory.
753
+
754
+ Without these options, you should not assume anything about the current
755
+ working directory when your executable is invoked. It can be the directory
756
+ where the executable is placed (when invoked through the Windows Explorer),
757
+ the users' current working directory (when invoking from the Command Prompt),
758
+ or even `C:\\WINDOWS\\SYSTEM32` when the executable is invoked through a file
759
+ association.
446
760
 
447
761
  To `require` additional files from the source directory while keeping the
448
762
  user's working directory:
449
763
 
450
764
  $LOAD_PATH.unshift File.dirname($0)
451
765
 
766
+ ### Finding files next to the executable
767
+
768
+ Be aware that `__FILE__`, `__dir__` and `$0` inside a packaged application
769
+ point to the extracted copy of your script in the temporary extraction
770
+ directory — not to the directory containing the `.exe`. Anchoring paths on
771
+ `__dir__` (e.g. `APP_ROOT = File.expand_path(__dir__)`) therefore makes the
772
+ application look for its data files inside the temporary directory, which is
773
+ usually not what you want.
774
+
775
+ To locate files relative to the executable, either build with
776
+ `--chdir-exe-dir` and use plain relative paths, or anchor your paths on the
777
+ `OCRAN_EXECUTABLE` environment variable, which is always set to the full path
778
+ of the running executable:
779
+
780
+ APP_ROOT = if ENV["OCRAN_EXECUTABLE"]
781
+ File.dirname(ENV["OCRAN_EXECUTABLE"])
782
+ else
783
+ __dir__ # plain `ruby myapp.rb` during development
784
+ end
785
+
452
786
  ### Detecting OCRAN at build time
453
787
 
454
788
  Check for the `Ocran` constant to detect whether OCRAN is currently building
@@ -527,6 +861,178 @@ file:
527
861
  end
528
862
  end
529
863
 
864
+ ### Packaging a Rails application
865
+
866
+ A Rails application with a SQLite database packages into a single
867
+ executable that serves HTTP. `test/test_rails.rb` builds one from scratch
868
+ on every run (`rails new`, `bin/rails generate scaffold`, plus controllers
869
+ and views written on the fly), packages it and drives a full CRUD round
870
+ trip through the resulting binary; read it as the worked example. It is
871
+ opt-in — run it with `OCRAN_RAILS_TEST=1 ruby -Itest test/test_rails.rb`.
872
+
873
+ Write a small entry script next to `config/` (Rails' own `config.ru` is
874
+ for a Rack server, not for OCRAN) and point OCRAN at it together with the
875
+ directories Rails reads at run time:
876
+
877
+ ocran server.rb app config db public --no-autoload --gem-all
878
+
879
+ Four things are worth knowing before you try it.
880
+
881
+ **`--no-autoload` is required.** OCRAN's autoload walker sweeps every
882
+ `Module` in `ObjectSpace` and tries to `const_get` each autoloaded
883
+ constant. Under Rails that means dragging in `I18n::Tests`,
884
+ `Prism::Translation` and everything else the framework declares but never
885
+ loads — minutes of output ending in an error. In production Rails eager
886
+ loads the application itself, so the walker has nothing to contribute.
887
+
888
+ **`--gem-all` is required.** Dependency detection is `$LOADED_FEATURES`
889
+ based, and Rails reads a great deal it never `require`s. The first thing
890
+ to break without it is `activesupport/lib/active_support/locale/en.rb`,
891
+ which I18n reads as a data file during boot. For the same reason the
892
+ application's own `app`, `config`, `db` and `public` directories have to
893
+ be named on the command line: ERB templates, `database.yml`, locale files
894
+ and migrations are not `require`d and so are invisible to the dependency
895
+ run.
896
+
897
+ **Gems Rails declares but never loads still have to be inside.** OCRAN
898
+ packs the gemspec of every gem it detects, and RubyGems activates the
899
+ complete declared dependency graph of a gemspec, not just the code you
900
+ actually load. `activesupport` declares `minitest` and `drb`, `railties`
901
+ declares `rake`; a running server loads none of them, so nothing puts them
902
+ in the package, and the executable then dies at startup with
903
+ `Gem::MissingSpecError`. Require them from the entry script:
904
+
905
+ begin
906
+ gem "minitest", "~> 5.0" # 5.x has no dependencies of its own
907
+ rescue Gem::LoadError
908
+ end
909
+ require "minitest" # activesupport
910
+ require "drb" # activesupport
911
+ require "rake" # railties
912
+
913
+ To find the equivalent list for your own application, boot it and diff the
914
+ declared dependencies against what is loaded:
915
+
916
+ Gem.loaded_specs.each_value do |spec|
917
+ spec.runtime_dependencies.each do |dep|
918
+ puts "#{dep.name} <- #{spec.name}" unless Gem.loaded_specs.key?(dep.name)
919
+ end
920
+ end
921
+
922
+ **Bundler.** The dependency run happens inside the OCRAN process, so a
923
+ `bundler/setup` in `config/boot.rb` confines it to your bundle; use
924
+ `--gemfile` to point OCRAN at the same `Gemfile`. The test takes the other
925
+ route and removes Bundler from the generated application entirely, letting
926
+ RubyGems activate the gems, which is why it needs neither `bundle install`
927
+ nor the network.
928
+
929
+ #### Where the database goes
930
+
931
+ **Not inside the package.** With the native stub the application directory
932
+ is a temporary directory that is created at every start and deleted when
933
+ the process exits: a database written there is thrown away. With
934
+ `--cosmo-ruby` it is a read-only ZIP store inside the executable and the
935
+ write simply fails.
936
+
937
+ Put mutable state next to the executable instead, using
938
+ `ENV["OCRAN_EXECUTABLE"]` (see [Environment variables](#environment-variables)),
939
+ and tell Rails about it before `config/environment` is loaded:
940
+
941
+ base = ENV["OCRAN_EXECUTABLE"] ? File.dirname(File.expand_path(ENV["OCRAN_EXECUTABLE"])) : __dir__
942
+ data_dir = File.join(base, "myapp-data")
943
+ FileUtils.mkdir_p(data_dir)
944
+
945
+ ENV["DATABASE_URL"] ||= "sqlite3:#{File.join(data_dir, "app.sqlite3")}"
946
+
947
+ `DATABASE_URL` is the cleanest lever, because `config/database.yml`
948
+ resolves relative paths against `Rails.root`, which is inside the package.
949
+ The same applies to everything else Rails writes:
950
+
951
+ * **tmp** (cache, pids, sockets) — set `config.paths["tmp"]`, or avoid it
952
+ altogether with `config.cache_store = :memory_store`.
953
+ * **log** — the generated production environment already logs to stdout;
954
+ if yours does not, set `config.logger` explicitly.
955
+ * **Active Storage** — point `config.active_storage.service` at a root
956
+ under your data directory.
957
+
958
+ Ship the schema in the package and apply it on first run, rather than
959
+ shipping a prebuilt database file:
960
+
961
+ ActiveRecord::MigrationContext.new(Rails.root.join("db/migrate").to_s).migrate
962
+
963
+ Calling this during the dependency run as well is what gets
964
+ `db/migrate/*.rb` into `$LOADED_FEATURES`, and therefore into the package.
965
+
966
+ #### Rails as a single portable executable (`--cosmo-ruby`)
967
+
968
+ The same application packages into one Actually Portable Executable that
969
+ runs on Linux, macOS, Windows and the BSDs without an installed Ruby:
970
+
971
+ ocran server.rb app config db public --no-autoload --gem-all \
972
+ --cosmo-ruby /path/to/ruby.com
973
+
974
+ No compiler is involved: `--cosmo-ruby` on its own selects the ZIP mode,
975
+ in which the application is injected into the interpreter's own ZIP store.
976
+ Nothing is unpacked at run time — the application is read straight out of
977
+ the executable. A Rails 8.1 application built this way measures **39.0 MB
978
+ and answers its first request 1.8 s after launch**, against 50.4 MB and
979
+ 1.5 s for the same application in a native OCRAN executable.
980
+
981
+ An APE cannot `dlopen`, so **every native extension has to be compiled
982
+ into the interpreter you point at**. For Rails that is `sqlite3`,
983
+ `nokogiri` (and with it libxml2 and libxslt), `puma`, `nio4r`,
984
+ `bigdecimal` and `racc`. CosmoRuby has had all of them since 4.0.6;
985
+ against an interpreter that has not, the build stops before producing
986
+ anything, naming the gem:
987
+
988
+ ERROR: Gem nokogiri-1.19.4-x86_64-linux-gnu is native (ships prebuilt
989
+ binaries (nokogiri.so)) and cannot run under the packed cosmopolitan
990
+ Ruby (x86_64-cosmo, static)
991
+
992
+ OCRAN drops the host copy of each gem the payload provides and lets the
993
+ payload's own serve. It recognises those either by a gemspec in the
994
+ payload or, failing that, by asking the payload whether it can resolve the
995
+ gem's primary feature at all: an extension linked into the binary, or a
996
+ library in its embedded stdlib (`cgi` and `pathname` are both), answers
997
+ `require` with no gemspec anywhere.
998
+
999
+ **One real limitation remains, and it is cryptographic.** CosmoRuby's
1000
+ `openssl` is a shim over MbedTLS: no `OpenSSL::Cipher`, no
1001
+ `OpenSSL::HMAC`, no PBKDF2, no `OpenSSL::Digest` class hierarchy. Rails
1002
+ does not merely use those, it *names* them at load time — `require "rails"`
1003
+ by itself dies on
1004
+
1005
+ active_support/message_encryptor.rb:116:in '<class:MessageEncryptor>':
1006
+ uninitialized constant OpenSSL::Cipher (NameError)
1007
+
1008
+ so an application packaged for such an interpreter has to do three things.
1009
+ `test/rails_app_generator.rb` does all three, and the file it writes,
1010
+ `openssl_gap.rb`, is a no-op on a Ruby with a complete `openssl`:
1011
+
1012
+ * **Fill in the missing pieces before Rails loads.** HMAC, PBKDF2,
1013
+ `OpenSSL.fixed_length_secure_compare` and an `OpenSSL::Digest` class
1014
+ hierarchy are a few lines each on top of Ruby's own `Digest`, and they
1015
+ are the real algorithms. `OpenSSL::Cipher` is not: it exists so that
1016
+ Rails can name the constant, and raises if anything tries to encrypt
1017
+ with it. A fake cipher would be worse than no cipher.
1018
+ * **Set `SECRET_KEY_BASE`, and ship no `config/credentials.yml.enc`.**
1019
+ Reading credentials decrypts them with AES-256-GCM and fails with
1020
+ `OpenSSL::Cipher::CipherError`. Dropping the file is the right move in
1021
+ any case: packaging it means packaging `config/master.key` beside it,
1022
+ i.e. handing the key to everyone who gets a copy of the executable.
1023
+ * **Keep the session off the cookie.** Rails' default `CookieStore`
1024
+ encrypts the session cookie, so it cannot work either;
1025
+ `config.session_store :cache_store` keeps the session server-side and
1026
+ puts only its id in the cookie. Signed cookies, the CSRF token and
1027
+ everything else that needs no more than an HMAC keep working, and a full
1028
+ scaffold CRUD round trip with CSRF token and session cookie is what the
1029
+ test drives through the packaged `.com`.
1030
+
1031
+ Active Record encryption, encrypted cookies and anything else that
1032
+ actually encrypts stay out of reach until CosmoRuby's `openssl` grows a
1033
+ cipher surface — mbedtls has AES, GCM and PKCS5, so it is a bounded job;
1034
+ it is tracked in `PORTING-NOTES.md` in the CosmoRuby repository.
1035
+
530
1036
  ## See elsewhere
531
1037
 
532
1038
  - [State of Ruby Packagers](https://gist.github.com/YOU54F/3775e66e6090e0371c11601e6b75c305)
@@ -534,10 +1040,12 @@ file:
534
1040
 
535
1041
  ## Credits
536
1042
 
537
- Lars Christensen and contributors for the OCRA project which this is forked from.
1043
+ [Andi Idogawa](https://github.com/largo), [Shinokaro](https://github.com/shinokaro)
1044
+ [Lars Christensen](https://github.com/larsch) and contributors for the OCRA project which this is forked from.
1045
+
1046
+ 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
1047
 
539
- Kevin Walzer of codebykevin, Maxim Samsonov for ocra2, John Mair for
540
- codesigning support.
1048
+ [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
1049
 
542
1050
  Igor Pavlov for the LZMA compressor and decompressor (Public Domain).
543
1051
 
@@ -551,7 +1059,7 @@ http://ruby.morphball.net/vit-ruby-ico_en.html).
551
1059
  (The MIT License)
552
1060
 
553
1061
  Copyright (c) 2009-2020 Lars Christensen
554
- Copyright (c) 2020-2025 The OCRAN Committers Team
1062
+ Copyright (c) 2020-2026 The OCRAN Committers Team
555
1063
 
556
1064
  Permission is hereby granted, free of charge, to any person obtaining
557
1065
  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__, ...)