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.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +37 -0
- data/README.md +539 -31
- data/lib/ocran/build_facade.rb +2 -0
- data/lib/ocran/cosmo_toolchain.rb +383 -0
- data/lib/ocran/dir_builder.rb +3 -0
- data/lib/ocran/direction.rb +819 -64
- 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 +172 -3
- data/lib/ocran/rubyopt_processor.rb +95 -0
- data/lib/ocran/runner.rb +84 -2
- data/lib/ocran/runtime_environment.rb +19 -1
- data/lib/ocran/stub_builder.rb +38 -6
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/zip_payload_builder.rb +282 -0
- data/lib/ocran/zip_writer.rb +287 -0
- data/src/Makefile +9 -0
- data/src/inst_dir.c +44 -2
- data/src/inst_dir.h +14 -0
- data/src/script_info.c +15 -2
- data/src/script_info.h +15 -1
- data/src/stub.c +70 -9
- data/src/system_utils.c +26 -0
- data/src/system_utils.h +14 -0
- data/src/system_utils_posix.c +46 -0
- data/src/unpack.c +8 -0
- data/src/unpack.h +19 -0
- metadata +15 -24
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,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
|
-
|
|
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
|
|
186
|
-
directory
|
|
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
|
|
324
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
data/lib/ocran/build_facade.rb
CHANGED