ocran 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d437febbbe8332ec9c9361a7ab7cd7084d323d1afc92e81105ff73e45217d194
4
- data.tar.gz: e4cc31077e27c6019037efaae65d552fcb72e3621a510773533629192f512dcb
3
+ metadata.gz: da09ddcc836fd862e4b8832daedf24ea3dc8c122779d19528e0d5611ef4b47ff
4
+ data.tar.gz: c231264021b94d991f197f9520ec5403445756707c0aca60c1841a72332754df
5
5
  SHA512:
6
- metadata.gz: a67462ca9e1187c8d83afa9c6665b166b0811680baf7a3c798f99cdd9fb7bcb2c3f8555f0acf88725e0fb803bc22c5fde24c40c14cf8142a62f1ffdba25ede1f
7
- data.tar.gz: 35fcb3c1de9f04c1b21a7de1c9d68c395d9fb6ea8563f3a891c5008f499138f03b533bf3d2546e19a57c3ae22243a705c5d9d4990d2e4dcdd4663d1b9b90074f
6
+ metadata.gz: bdc488641cfdca101ebd4e2b01168838fd15d3eb418606ebaf2b2af431a08fa9e92da3ecf959ee0a485f669ea79af0707e842396011916d37365f8e553eeadcf
7
+ data.tar.gz: ae524b14a5b1198a8f9f761b6e38ee0af06cb6843827b10dade4d9fb8d37585066cee7866ca3fa554f03825f7293a92f32bed0e28cea0380cc3ee5d3f7062154
data/CHANGELOG.txt CHANGED
@@ -1,3 +1,17 @@
1
+ === 1.5.1
2
+ - Releases carry the three `--static-runtime` runtimes (Windows zip, Linux and macOS tarballs) next to the gems: the release workflow calls the runtime workflows, which build, test and attest them, and attaches what they built (a published release is immutable, so they cannot be added afterwards). A manual run of the release workflow is a dry run that builds and tests everything a release would contain and publishes nothing. The runtime workflows run on branch pushes only; for a tag the release workflow runs them.
3
+ - The macOS runtime and macOS executables are reproducible: the runtime build sets ZERO_AR_DATE=1 (Apple's ar, libtool and ranlib wrote each member's modification time into the static libraries, and ld the objects' times into the executable's debug map), and OCRAN links macOS executables with -Wl,-S, without a debug map naming objects compiled in a temporary directory. The runtime workflows package the smoke application twice and require the same bytes.
4
+ - A bare bin/ruby(.exe) of a packaged application gets the launch script's environment: OCRAN packs a RubyGems startup hook (rubygems/defaults/operating_system.rb in the packed site_ruby) that sets the variables the launch script exports (GEM_HOME, GEM_PATH, RUBYLIB, SSL_CERT_FILE/SSL_CERT_DIR when a CA bundle is packed, ...) when they are not set, with paths relative to where the package is, and then loads the Ruby's own hook (RubyInstaller's). A Windows service or scheduled task registered as `bin\ruby.exe src\service.rb` - the way a service has to run, since the service control manager only accepts the process it started itself - could not load the packaged gems (LoadError) and used the account's own gem directory. New README section "Running as a Windows service".
5
+ - The standard library's unicode_normalize files are always packed. String#unicode_normalize is core but loads its implementation and tables from the standard library on first use, so an application that did not call it during the dependency run failed once packed with "cannot load such file -- unicode_normalize/normalize.rb".
6
+ - A build that needs a launcher stub which was never compiled (OCRAN used from a git checkout, e.g. a Bundler git source) fails at once with a message saying how to build it (`make -C src install`; `ridk exec make -C src install` with RubyInstaller) or, for directory and zip output, to pass --no-wrapper-exe; it used to fail with a bare ENOENT for share/ocran/stub.exe after the whole dependency run.
7
+ - `rake build` and `rake build_stub` work without the development bundle installed: the Rakefile sets up Bundler only for tasks other than building the stubs.
8
+ - --verbose reports "Failed to load gemspec" only for files in gem directories; it listed every standard library file the application loaded (hundreds of lines on Ruby 3.4), which buried real errors.
9
+ - The release workflow pushes every gem to rubygems.org with a Sigstore attestation (signed with sigstore-cli, as rubygems/release-gem does), so rubygems.org shows the gems as built and signed on GitHub Actions; the GitHub attestation alone does not reach rubygems.org.
10
+ - A library that exits while OCRAN loads its autoload constants no longer ends the build: it is reported as a warning, like an autoload that raises. rdoc/servlet.rb aborts when webrick is missing (Ruby 3.4 no longer ships it), so packaging an application that loaded rdoc failed with exit 1, and the abort message went to stderr ahead of the buffered progress output, at the start of the log rather than the end.
11
+ - Packaging no longer aborts with "No such file or directory" over an entry of $LOADED_FEATURES that has no file behind it. A racc parser generated with its runtime embedded marks <dir>/racc/parser.rb and racc/info.rb as loaded so that racc is not loaded twice; rdoc's RD parsers in Ruby 3.4 do, so an application that loaded them (directly or through a gem) failed with ".../lib/ruby/3.4.0/rdoc/rd/racc/parser.rb (Errno::ENOENT)" (reported on a Windows runner with Ruby 3.4.11 and ocran 1.4.6). Such entries are skipped; --verbose names them.
12
+ - `--add-all-core` no longer aborts over an entry of the Ruby library directories that is not a readable file, e.g. Debian's packaged Ruby, whose rdoc fonts are links into fonts-lato: it packs the rest and warns which entries it left out. A configured library directory that does not exist (Debian's sitelibdir, /usr/local/lib/site_ruby/<version>) is skipped; it failed with "Unexpected library path format".
13
+ - README: a one-line definition and quick start at the top, an FAQ, a comparison with other Ruby packagers and a section on security, dependencies and reproducibility; llms.txt summarises OCRAN for LLM-based search. The gemspec links the issue tracker and the README (bug_tracker_uri, documentation_uri) for rubygems.org.
14
+
1
15
  === 1.5.0
2
16
  - CI builds all three `--static-runtime` runtimes and tests them before publishing each as a workflow artifact: new static-runtime-windows.yml cross-builds the Windows runtime on Linux (Debian 13, in the official ruby image pinned by digest) and tests it on Windows (DLL imports, executables packaged on Linux run with a minimal PATH, packaging on Windows with nokogiri built by MSYS2's gcc). Each runtime is published as one reproducible archive (runtime/pack-runtime.rb; a zip for Windows, a .tar.gz for Linux and macOS), and a runtime that passed its tests gets a signed build provenance attestation for the archive and its runtime executable: `gh attestation verify <file> -R Largo/ocran`. runtime/smoke/run.rb runs executables with a minimal PATH and without Ruby's environment variables, and can run executables packaged elsewhere (OCRAN_SMOKE_PREBUILT=1).
3
17
  - The release workflow attests every gem it publishes (signed build provenance): `gh attestation verify ocran-<version>-<platform>.gem -R Largo/ocran` checks a gem from the GitHub release or from rubygems.org (`gem fetch ocran`).
data/README.md CHANGED
@@ -1,12 +1,29 @@
1
- # OCRAN
1
+ # OCRAN — turn Ruby scripts into standalone executables (.exe) for Windows, Linux and macOS
2
2
 
3
3
  ![OCRAN: ship your Ruby app as one file, no Ruby needed to run it](https://raw.githubusercontent.com/Largo/ocran/master/docs/social/twitter-card.png)
4
4
 
5
- [OCRAN (One-Click Ruby Application Next)](https://github.com/largo/ocran) packages Ruby applications for
5
+ [![Gem Version](https://badge.fury.io/rb/ocran.svg)](https://rubygems.org/gems/ocran)
6
+
7
+ **OCRAN (One-Click Ruby Application Next) is a Ruby-to-executable packager:
8
+ `ocran app.rb` turns a Ruby script into a single `.exe` (Windows) or native
9
+ binary (Linux, macOS) that runs on machines without Ruby installed.** It is
10
+ the maintained successor of [OCRA](https://github.com/larsch/ocra) for
11
+ Ruby 3.3 and newer.
12
+
13
+ [OCRAN](https://github.com/largo/ocran) packages Ruby applications for
6
14
  distribution. It bundles your script, the Ruby interpreter, gems, and native
7
15
  libraries into a single self-contained artifact that runs without requiring
8
16
  Ruby to be installed on the target machine.
9
17
 
18
+ Quick start:
19
+
20
+ ```
21
+ gem install ocran
22
+ ocran app.rb # → app.exe on Windows, ./app on Linux/macOS
23
+ ```
24
+
25
+ See the [FAQ](#faq) and the [comparison with other Ruby packagers](#comparison-with-other-ruby-packagers).
26
+
10
27
  OCRAN supports four output formats, all cross-platform:
11
28
 
12
29
  * **Self-extracting executable** — bundles everything into a single binary that unpacks and runs transparently, with no Ruby installation required. Produces a `.exe` on Windows, and a native executable on Linux and macOS.
@@ -256,7 +273,8 @@ goes through these steps in order of impact.
256
273
  built. CI builds and tests the three runtimes and publishes each as a
257
274
  reproducible archive (a zip for Windows, a tarball otherwise) with a
258
275
  signed build provenance attestation
259
- (`gh attestation verify <archive> -R Largo/ocran`).
276
+ (`gh attestation verify <archive> -R Largo/ocran`); releases from 1.5.1
277
+ on have them attached.
260
278
 
261
279
  Dependency detection runs as usual, under the build host's Ruby, which
262
280
  must have the runtime's API version (e.g. 4.0.0). The standard library
@@ -643,6 +661,41 @@ On Windows, run the batch file:
643
661
 
644
662
  myapp\script.bat
645
663
 
664
+ ### Running as a Windows service
665
+
666
+ The self-extracting `.exe`, the wrapper `.exe` and the launch script all
667
+ start the packed `ruby.exe` as a child process. The Windows service control
668
+ manager accepts the service dispatcher only from the process it started
669
+ itself, so a service registered through any of them fails with error 1063
670
+ ("The service process could not connect to the service controller").
671
+
672
+ Build with `--output-dir` (or `--innosetup`) instead and register the packed
673
+ interpreter directly, e.g. with `sc create` or the
674
+ [win32-service](https://github.com/chef/win32-service) gem's
675
+ `Win32::Service.create`:
676
+
677
+ ocran service.rb --output-dir C:\MyService
678
+ sc create MyService binPath= "C:\MyService\bin\ruby.exe C:\MyService\src\service.rb"
679
+
680
+ That bare `ruby.exe` still gets the package's environment: OCRAN packs a
681
+ RubyGems startup hook (`lib/ruby/site_ruby/<version>/rubygems/defaults/operating_system.rb`)
682
+ that sets `GEM_HOME`, `GEM_PATH`, `RUBYLIB`, `SSL_CERT_FILE` and the other
683
+ variables of the launch script to the directory's actual location, unless
684
+ they are already set, clears those the launch script clears
685
+ (`BUNDLE_GEMFILE`), and then loads the Ruby installation's own one
686
+ (RubyInstaller's). OCRAN leaves the hook out, and warns, when the building
687
+ Ruby has such a file in its `site_ruby` already or keeps `site_ruby` outside
688
+ its installation directory; then set those variables in the service's
689
+ environment yourself. The hook does not change the working directory (a service
690
+ starts in `C:\Windows\System32`), so find your files from `__dir__`. If the
691
+ script the launch script runs is a generated `ocran-rubyopt-launcher.rb`
692
+ (`-I`/`-r` in `RUBYOPT`), register that one.
693
+
694
+ OCRAN runs the script at build time, where the service's `mainloop` cannot
695
+ work: skip it with `defined?(Ocran)` (see "Detecting OCRAN at build time")
696
+ and `require` there what the service only loads later, such as `net/http`,
697
+ so that it gets packed.
698
+
646
699
  ### Pitfalls:
647
700
 
648
701
  * Avoid modifying load paths at runtime. Use `-I` or `RUBYLIB` if needed, but don't expect OCRAN to preserve them for runtime. OCRAN may pack sources into other directories than you
@@ -1320,10 +1373,176 @@ actually encrypts stay out of reach until CosmoRuby's `openssl` grows a
1320
1373
  cipher surface — mbedtls has AES, GCM and PKCS5, so it is a bounded job;
1321
1374
  it is tracked in `PORTING-NOTES.md` in the CosmoRuby repository.
1322
1375
 
1323
- ## See elsewhere
1376
+ ## FAQ
1377
+
1378
+ ### How do I convert a Ruby script into an .exe?
1324
1379
 
1325
- - [State of Ruby Packagers](https://gist.github.com/YOU54F/3775e66e6090e0371c11601e6b75c305)
1326
- - [Traveling Ruby](https://github.com/trubygems/traveling-ruby)
1380
+ Install OCRAN on Windows and run it on your entry script:
1381
+
1382
+ gem install ocran
1383
+ ocran app.rb
1384
+
1385
+ The result is `app.exe`, which contains your script, the Ruby interpreter,
1386
+ the gems it loaded and the DLLs they need. On Linux and macOS the same
1387
+ command produces a native executable `./app`.
1388
+
1389
+ ### Does the target machine need Ruby installed?
1390
+
1391
+ No. The executable carries its own Ruby interpreter, standard library,
1392
+ gems and native libraries.
1393
+
1394
+ ### Can I build a Windows .exe on Linux or macOS?
1395
+
1396
+ Not with the default mode: OCRAN bundles the Ruby of the machine it runs
1397
+ on, so build a Windows `.exe` on Windows (for example on a GitHub Actions
1398
+ Windows runner, see
1399
+ [Building for multiple platforms](#building-for-multiple-platforms-with-github-actions)),
1400
+ or run a Windows Ruby and OCRAN under Wine on Linux/macOS.
1401
+ Two experimental modes package without the target's own Ruby:
1402
+ `--static-runtime` with a Windows runtime builds a Windows `.exe` on
1403
+ Linux (and the other way round), and `--cosmo-ruby` packages a
1404
+ cosmopolitan Ruby into one file that runs on Windows, Linux and macOS.
1405
+
1406
+ ### What is the difference between OCRA and OCRAN?
1407
+
1408
+ OCRAN is a maintained fork of OCRA. OCRA targets Windows only and is no
1409
+ longer updated for current Ruby versions; OCRAN supports Ruby 3.3+, builds
1410
+ for Windows, Linux and macOS, and adds directory, zip, macOS `.app` and
1411
+ Inno Setup output. To migrate, call `ocran` instead of `ocra` and replace
1412
+ `OCRA_EXECUTABLE` with `OCRAN_EXECUTABLE` in your code.
1413
+
1414
+ ### Which gems and native extensions are supported?
1415
+
1416
+ OCRAN runs your script once at build time and records every file that
1417
+ `require` and `load` pull in, including compiled extensions and the DLLs
1418
+ or shared libraries they link. In the default mode, gems with native extensions
1419
+ (for example `sqlite3`, `pg`, `nokogiri`) are packaged along with their
1420
+ libraries; the experimental `--cosmo-ruby` mode only supports the native
1421
+ extensions built into its interpreter. Use
1422
+ `--gemfile` to include everything from a Bundler `Gemfile`, and the
1423
+ [gem detection modes](#gem-content-detection-modes) to include extra files.
1424
+
1425
+ ### Can I package a Rails, GUI or command-line app?
1426
+
1427
+ Yes. Command-line tools work out of the box, GUI apps can use
1428
+ `--windows` to hide the console on Windows, and Rails apps with SQLite are
1429
+ covered in [Packaging a Rails application](#packaging-a-rails-application).
1430
+
1431
+ ### Why does my packaged app start slowly?
1432
+
1433
+ The default self-extracting executable unpacks Ruby to a temporary
1434
+ directory on every launch. Use `--output-dir`, `--output-zip` or
1435
+ `--innosetup` to install the files once, `--static-runtime` for a single
1436
+ executable that runs without extracting, or `--cosmo-ruby` with an
1437
+ interpreter that supports ZIP packaging (CosmoRuby `v4.0.6-cosmo3` or
1438
+ newer) to run without extracting; with other interpreters `--cosmo-ruby`
1439
+ falls back to a launcher stub that extracts on every start.
1440
+
1441
+ ### My antivirus flags the generated .exe. What can I do?
1442
+
1443
+ Self-extracting executables that unpack an interpreter are a common
1444
+ heuristic trigger. Sign the executable (OCRAN output is code-signing
1445
+ compatible, e.g. with `signtool` on Windows), ship an
1446
+ installer built with `--innosetup`, or ship a plain directory with
1447
+ `--output-dir`.
1448
+
1449
+ ## Comparison with other Ruby packagers
1450
+
1451
+ | Tool | Output | Platforms | Single file | Maintained for current Ruby |
1452
+ |------|--------|-----------|-------------|--------------------------|
1453
+ | **OCRAN** | `.exe` / native binary, directory, zip, macOS `.app`, Inno Setup installer | Windows, Linux, macOS | Yes | Yes |
1454
+ | [OCRA](https://github.com/larsch/ocra) | `.exe` | Windows only | Yes | No |
1455
+ | [Traveling Ruby](https://github.com/trubygems/traveling-ruby) | Prebuilt Ruby runtime you ship next to your app | Windows, Linux, macOS | No | Yes |
1456
+ | [Tebako](https://github.com/tamatebako/tebako) | Single binary with an embedded filesystem | Windows, Linux, macOS | Yes | Yes |
1457
+ | [ruby-packer](https://github.com/pmq20/ruby-packer) | Single binary with Ruby compiled from source | Windows, Linux, macOS | Yes | No |
1458
+
1459
+ OCRAN's distinguishing features are that, in its default mode, it needs no
1460
+ C compiler to package an app (it reuses your installed Ruby), detects dependencies by running
1461
+ your script, and can produce installers and macOS app bundles directly.
1462
+
1463
+ See also: [State of Ruby Packagers](https://gist.github.com/YOU54F/3775e66e6090e0371c11601e6b75c305).
1464
+
1465
+ ## Security, dependencies and reproducibility
1466
+
1467
+ A packager decides which interpreter, libraries and gems end up on your
1468
+ users' machines, so where those come from matters as much as the
1469
+ packaging itself. OCRAN is built so that nothing reaches the executable
1470
+ that you did not install, run and test yourself, and so that you can
1471
+ check what went in.
1472
+
1473
+ **No hidden downloads.** In its default mode OCRAN makes no network
1474
+ requests: it does not fetch an interpreter, prebuilt runtime, patched
1475
+ Ruby source or library tarball while packaging. The executables it
1476
+ produces, in every mode, never download a runtime when they start. Some
1477
+ packaging approaches ship a small launcher that pulls the actual runtime
1478
+ from the internet on first launch; the app then depends on a server
1479
+ being reachable and trustworthy long after you released it. An OCRAN
1480
+ executable carries the interpreter, gems and libraries it runs; whether
1481
+ your app uses the network is up to your own code. The opt-in modes that
1482
+ do fetch say so and verify what they get:
1483
+
1484
+ * `--wasm` downloads the ruby.wasm or PicoRuby package from the npm
1485
+ registry once, checks it against the SHA-512 integrity the registry
1486
+ publishes, and caches it, so later builds run offline.
1487
+ * `--static-runtime` fetches the source gem of a precompiled platform gem
1488
+ from your configured gem sources when it has to build that gem's
1489
+ extension for the runtime.
1490
+
1491
+ **Your Ruby, from the source you already trust.** By default OCRAN
1492
+ packages the Ruby interpreter installed on the build machine —
1493
+ RubyInstaller, a distribution package, `ruby-build`, `ruby/setup-ruby` in
1494
+ CI, or your own build. It does not substitute a separately built or
1495
+ patched interpreter from a third party. Security fixes reach your app the
1496
+ same way they reach your development machine: update Ruby, rebuild.
1497
+ Packagers that embed their own copy of Ruby and its C libraries (OpenSSL,
1498
+ zlib, libyaml, libffi, ...) freeze those versions at whatever the
1499
+ packager last shipped, and fall behind on security fixes when the
1500
+ packager stops being updated. The `--static-runtime` runtimes are built
1501
+ by scripts in this repository (`runtime/`) that you can run yourself;
1502
+ OCRAN checks a runtime against the SHA-256 recorded in its
1503
+ `runtime.json`.
1504
+
1505
+ **Exactly the gems and libraries you tested.** Dependencies are not
1506
+ resolved again at packaging time. OCRAN runs your script, records the
1507
+ files Ruby actually loaded, and packs those files from your installed
1508
+ gems — the versions pinned in your `Gemfile.lock` when you build under
1509
+ Bundler. Native libraries are taken from the same installation your
1510
+ extensions were loaded from. There is no second dependency resolution
1511
+ that could pick a different version than the one you ran.
1512
+
1513
+ **Reproducible builds.** The bundled interpreter and dependencies are
1514
+ determined by three things you control: the Ruby installation (or static
1515
+ runtime), the installed gems (or `Gemfile.lock`) and the OCRAN version;
1516
+ the rest of the artifact follows from your application source and the
1517
+ OCRAN options you pass. Pin them — e.g. a fixed `ruby-version` in
1518
+ `ruby/setup-ruby` plus a committed `Gemfile.lock` — and every build
1519
+ packages the same interpreter, gems and libraries. OCRAN's own stubs are
1520
+ built reproducibly (no timestamps in the PE header), the Windows, Linux
1521
+ and macOS static runtimes are reproducible, and `--static-runtime`
1522
+ executables are byte-reproducible on all three systems; CI packages a
1523
+ test application twice and requires the same bytes.
1524
+
1525
+ **Inspect what went in.** Build with `--output-dir` and look at the
1526
+ directory, or run an executable with `--debug-extract`. A
1527
+ `--static-runtime` executable is a plain ZIP archive behind the runtime:
1528
+ `unzip -l app.exe` lists exactly what will run, and
1529
+ `ocran/manifest.sha256` inside it records the SHA-256 of the runtime and
1530
+ of every file.
1531
+
1532
+ **Verifiable OCRAN releases.** The launcher stubs (`stub.exe`,
1533
+ `stubw.exe` and the Linux/macOS `stub`) are about 4,000 lines of C in
1534
+ `src/`, compiled from this repository by GitHub Actions. Released gems
1535
+ and the CI-built static runtimes carry signed build provenance
1536
+ attestations (`gh attestation verify <file> -R Largo/ocran`); from 1.5.1
1537
+ on, each GitHub release has the three runtimes attached next to the gems,
1538
+ and gems are pushed to rubygems.org with Sigstore attestations. The only
1539
+ third-party binary is `lzma.exe`, taken unmodified from the official
1540
+ 7-Zip release (see [Safety](#safety)).
1541
+
1542
+ The trade-off: keeping the bundled Ruby patched is your job, just as it
1543
+ is for the Ruby you develop with. The experimental `--cosmo-ruby` mode
1544
+ packages a cosmopolitan interpreter file you choose, such as a CosmoRuby
1545
+ release, as given.
1327
1546
 
1328
1547
  ## Credits
1329
1548
 
@@ -30,9 +30,16 @@ module Ocran
30
30
 
31
31
  def export(name, value)
32
32
  verbose "export #{name}=#{replace_placeholder(value)}"
33
+ exported_env[name.to_s] = value.to_s
33
34
  super
34
35
  end
35
36
 
37
+ # The variables exported so far (name => value with the extraction root
38
+ # placeholder), whatever the builder does with them.
39
+ def exported_env
40
+ @exported_env ||= {}
41
+ end
42
+
36
43
  def replace_placeholder(s)
37
44
  s.to_s.gsub(EXTRACT_ROOT.to_s, "<tempdir>")
38
45
  end
@@ -6,6 +6,7 @@ require_relative "refine_pathname"
6
6
  require_relative "host_config_helper"
7
7
  require_relative "command_output"
8
8
  require_relative "build_constants"
9
+ require_relative "env_hook"
9
10
 
10
11
  module Ocran
11
12
  class Direction
@@ -396,19 +397,30 @@ module Ocran
396
397
  # Convert all relative paths to absolute paths before building.
397
398
  # NOTE: In the future, different strategies may be needed before and after script execution.
398
399
  features.filter_map do |feature|
399
- if feature.absolute?
400
- feature
401
- elsif (load_path = @post_env.find_load_path(feature))
402
- feature.expand_path(@post_env.expand_path(load_path))
403
- else
404
- # This message occurs when paths for core library files (e.g., enumerator.so,
405
- # rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
406
- # found. These are integral to Ruby's standard libraries or extensions and
407
- # may not be located via normal load path searches, especially in RubyInstaller
408
- # environments.
409
- verbose "Load path not found for #{feature}, skip this feature"
410
- nil
400
+ path =
401
+ if feature.absolute?
402
+ feature
403
+ elsif (load_path = @post_env.find_load_path(feature))
404
+ feature.expand_path(@post_env.expand_path(load_path))
405
+ else
406
+ # This message occurs when paths for core library files (e.g., enumerator.so,
407
+ # rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
408
+ # found. These are integral to Ruby's standard libraries or extensions and
409
+ # may not be located via normal load path searches, especially in RubyInstaller
410
+ # environments.
411
+ verbose "Load path not found for #{feature}, skip this feature"
412
+ next
413
+ end
414
+ # An entry can be in $LOADED_FEATURES without a file behind it: a
415
+ # racc parser generated with its runtime embedded marks
416
+ # <dir>/racc/parser.rb and racc/info.rb as loaded so that racc is not
417
+ # loaded twice (rdoc's RD parsers in Ruby 3.4 do). Copying it failed
418
+ # the whole build with ENOENT; there is nothing to pack.
419
+ unless path.file?
420
+ verbose "#{path} is marked as loaded but is not a file, skip this feature"
421
+ next
411
422
  end
423
+ path
412
424
  end
413
425
  end
414
426
 
@@ -471,6 +483,19 @@ module Ocran
471
483
  end
472
484
  end
473
485
 
486
+ # String#unicode_normalize (and unicode_normalized?, unicode_normalize!)
487
+ # is core, but core requires its implementation from the standard
488
+ # library (unicode_normalize/normalize.rb, which loads tables.rb) on
489
+ # first use. An app that does not call it during the dependency run
490
+ # fails with a LoadError once packed, so always pack these files.
491
+ # Not with --cosmo-ruby: the payload has its own standard library.
492
+ unless @option.cosmo_ruby
493
+ known = features.to_set { |f| f.path_key }
494
+ (Pathname(RbConfig::CONFIG["rubylibdir"]) / "unicode_normalize").glob("*.rb").sort.each do |path|
495
+ features << path unless known.include?(path.path_key)
496
+ end
497
+ end
498
+
474
499
  say "Building #{@option.output_executable}"
475
500
  require_relative "build_helper"
476
501
  builder.extend(BuildHelper)
@@ -955,14 +980,23 @@ module Ocran
955
980
  elsif @option.add_all_core?
956
981
  say "Will include all ruby core libraries"
957
982
  all_core_dir.each do |path|
983
+ # A configured directory that was never created (Debian's
984
+ # sitelibdir, /usr/local/lib/site_ruby/<ver>) holds nothing.
985
+ next unless path.directory?
986
+
958
987
  # Match the load path against standard library, site_ruby, and vendor_ruby paths
959
988
  unless (subdir = path.to_posix.match(RUBY_LIBRARY_PATH_REGEX)&.[](1))
960
989
  raise "Unexpected library path format (does not match core dirs): #{path}"
961
990
  end
962
- path.find.each do |src|
963
- next if src.directory?
991
+ files, skipped = self.class.core_library_files(path)
992
+ files.each do |src|
964
993
  builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
965
994
  end
995
+ next if skipped.empty?
996
+
997
+ warning "Not packed: #{skipped.size} entr#{skipped.size == 1 ? "y" : "ies"} of #{path} that " \
998
+ "#{skipped.size == 1 ? "is" : "are"} not a readable file (#{skipped.first(3).join(", ")}" \
999
+ "#{skipped.size > 3 ? ", ..." : ""})"
966
1000
  end
967
1001
  end
968
1002
 
@@ -1215,6 +1249,8 @@ module Ocran
1215
1249
  end
1216
1250
  builder.set_env_path("GEM_PATH", *gem_paths)
1217
1251
 
1252
+ pack_env_hook(builder)
1253
+
1218
1254
  # Add the opcode to launch the script
1219
1255
  installed_ruby_exe = BINDIR / ruby_executable
1220
1256
  target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
@@ -1224,6 +1260,58 @@ module Ocran
1224
1260
  builder.exec(installed_ruby_exe, target_script, *@option.argv)
1225
1261
  end
1226
1262
 
1263
+ # Packs the RubyGems startup hook that sets the exported variables for a
1264
+ # bare bin/ruby of the package (see EnvHook), e.g. a Windows service
1265
+ # registered as `bin\ruby.exe src\service.rb`. It goes into the packed
1266
+ # sitelibdir, the first default load path entry, in every output that
1267
+ # carries the host Ruby: the stub and the launch scripts set the
1268
+ # variables themselves, so there it only loads the original file.
1269
+ # Not with --cosmo-ruby and --static-runtime, which bring their own
1270
+ # Ruby and set up their environment themselves.
1271
+ def pack_env_hook(builder)
1272
+ return if @option.cosmo_ruby || @option.static_runtime?
1273
+
1274
+ site = Pathname(sitelibdir)
1275
+ unless site.subpath?(exec_prefix)
1276
+ verbose "Not packing #{EnvHook::FEATURE}: sitelibdir #{site} is outside #{exec_prefix}"
1277
+ return
1278
+ end
1279
+ if (site / EnvHook::FEATURE).exist?
1280
+ warning "Not packing ocran's #{EnvHook::FEATURE}: #{site} has one already, so a bare " \
1281
+ "#{BINDIR / ruby_executable} of the package will not get the launch script's environment"
1282
+ return
1283
+ end
1284
+
1285
+ target = site.relative_path_from(exec_prefix) / EnvHook::FEATURE
1286
+ say "Adding #{target} (sets the launch environment for a bare #{BINDIR / ruby_executable})"
1287
+ require "tempfile"
1288
+ hook_file = Tempfile.new(["ocran-operating_system", ".rb"])
1289
+ hook_file.write(EnvHook.source(builder.exported_env, target.dirname.each_filename.count))
1290
+ hook_file.close
1291
+ verbose File.read(hook_file.path)
1292
+ # Kept until the build finishes, like the RUBYOPT launcher.
1293
+ @env_hook_file = hook_file
1294
+ builder.cp(hook_file.path, target)
1295
+ end
1296
+ private :pack_env_hook
1297
+
1298
+ # The files below +dir+ that --add-all-core packs, and the entries it
1299
+ # cannot: a Ruby installation can list what is not there - Debian's
1300
+ # rdoc links its fonts into a fonts-lato package that may not be
1301
+ # installed, and a stdlib entry that vanished or cannot be read used to
1302
+ # abort the whole build (ENOENT) over a file no application needs.
1303
+ # Returns [files, skipped], both lists of Pathname.
1304
+ def self.core_library_files(dir)
1305
+ files = []
1306
+ skipped = []
1307
+ dir.find do |src|
1308
+ next if src.directory?
1309
+
1310
+ (src.file? && src.readable? ? files : skipped) << src
1311
+ end
1312
+ [files, skipped]
1313
+ end
1314
+
1227
1315
  # Maps an absolute build-machine path to its location (relative to the
1228
1316
  # extraction root) inside the packed application, mirroring where
1229
1317
  # #construct places files. Returns nil when the path is not packed.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+ require_relative "build_constants"
3
+
4
+ module Ocran
5
+ # The RubyGems startup hook that gives a bare interpreter of a packed
6
+ # application the environment of its launch script.
7
+ #
8
+ # The launch script (app.bat / app.sh), the wrapper executable and the
9
+ # self-extracting stub all set the variables Direction#construct exports
10
+ # (GEM_HOME, GEM_PATH, RUBYLIB, SSL_CERT_FILE, ...) before they start
11
+ # bin/ruby. Some hosts can only start the interpreter itself: the Windows
12
+ # service control manager accepts the service dispatcher only from the
13
+ # process it started (error 1063), so a service registered as
14
+ # `bin\ruby.exe src\service.rb` runs without any of them - gems packed
15
+ # outside the Ruby installation are not found, and the CA bundle ocran
16
+ # packed is only used where OpenSSL happens to look on its own.
17
+ #
18
+ # RubyGems requires "rubygems/defaults/operating_system" while it loads,
19
+ # before it computes its paths and before anything of the application
20
+ # runs; distributions use that file to customise RubyGems (RubyInstaller
21
+ # sets up its DLL search paths in it). ocran packs its own file of that
22
+ # name in the packed sitelibdir, which comes first on the default load
23
+ # path. It sets each variable the launch script would set and that is
24
+ # not set yet, resolved against the directory the package is in at run
25
+ # time, and then requires the next operating_system.rb on the load path
26
+ # - the distribution's own - so that keeps working as before.
27
+ #
28
+ # Under the launch script or the stub every variable is already set, so
29
+ # the hook only passes on to the original file there.
30
+ module EnvHook
31
+ include BuildConstants
32
+
33
+ # The feature RubyGems requires at startup.
34
+ FEATURE = "rubygems/defaults/operating_system.rb"
35
+
36
+ # Builds the hook's source for the variables in +env+ (name => value as
37
+ # exported at build time, with the extraction root placeholder) for a
38
+ # hook packed +depth+ directories below the package root (the depth of
39
+ # the packed sitelibdir plus the two of FEATURE's own directories).
40
+ def self.source(env, depth)
41
+ root_rel = ([".."] * depth).join("/")
42
+ entries = env.map { |name, value|
43
+ " #{name.to_s.dump} => #{value.to_s.dump},"
44
+ }
45
+
46
+ <<~RUBY
47
+ # frozen_string_literal: true
48
+ # Generated by OCRAN. Gives a bare bin/ruby of this package the
49
+ # environment of the launch script: each variable below that is not
50
+ # set yet is set, with "#{EXTRACT_ROOT}/" standing for the package root,
51
+ # and each one the launch script empties is removed, as it does,
52
+ # and then the next #{FEATURE} on the load path (the one the Ruby
53
+ # installation ships, if any) is loaded, as RubyGems would have done.
54
+ # RUBYOPT, LD_LIBRARY_PATH and DYLD_LIBRARY_PATH take effect for child
55
+ # processes only: they are read before this file runs.
56
+ ocran_env = {
57
+ #{entries.join("\n")}
58
+ }
59
+ ocran_root = File.expand_path(#{root_rel.dump}, __dir__)
60
+ ocran_windows = File::ALT_SEPARATOR == "\\\\"
61
+ ocran_same = lambda do |entry, dir|
62
+ entry = File.expand_path(entry)
63
+ ocran_windows ? entry.casecmp?(dir) : entry == dir
64
+ end
65
+ ocran_feature = lambda { |dir| File.join(File.expand_path(dir), #{FEATURE.dump}) }
66
+ ocran_this = lambda { |file| File.file?(file) && File.identical?(file, __FILE__) }
67
+ # RUBYLIB goes where Ruby would have put it: just before the
68
+ # directory this file was found in, the first default entry.
69
+ ocran_index = $LOAD_PATH.index { |dir| ocran_this.call(ocran_feature.call(dir)) } || 0
70
+ ocran_gem_paths = false
71
+ ocran_env.each do |name, value|
72
+ # Removed by the launch script, whatever the parent set: an
73
+ # inherited BUNDLE_GEMFILE must not replace the packaged Gemfile.
74
+ if value.empty?
75
+ ENV.delete(name)
76
+ next
77
+ end
78
+ next if ENV.key?(name)
79
+
80
+ value = value.gsub(%r{#{Regexp.escape(EXTRACT_ROOT.to_s)}[/\\\\]}) { "\#{ocran_root}/" }
81
+ value = value.tr("/", "\\\\") if ocran_windows
82
+ ENV[name] = value
83
+ case name
84
+ when "GEM_HOME", "GEM_PATH"
85
+ ocran_gem_paths = true
86
+ when "RUBYLIB"
87
+ dirs = value.split(File::PATH_SEPARATOR).reject(&:empty?).map { |dir| File.expand_path(dir) }
88
+ dirs.reject! { |dir| $LOAD_PATH.any? { |entry| ocran_same.call(entry, dir) } }
89
+ $LOAD_PATH.insert(ocran_index, *dirs.uniq)
90
+ end
91
+ end
92
+ Gem.clear_paths if ocran_gem_paths && defined?(Gem.clear_paths)
93
+ ocran_original = $LOAD_PATH.map(&ocran_feature).find { |file| File.file?(file) && !ocran_this.call(file) }
94
+ require ocran_original if ocran_original
95
+ RUBY
96
+ end
97
+ end
98
+ end
@@ -90,8 +90,12 @@ module Ocran
90
90
  # Skip if found in known Gem dir
91
91
  elsif (spec = GemSpecQueryable.find_spec(feature))
92
92
  gems << spec
93
- else
94
- puts "Failed to load gemspec for #{feature}" if verbose
93
+ elsif verbose && Gem.path.any? { |gem_path| feature.subpath?(File.join(gem_path, "gems")) }
94
+ # Only a feature in a gem directory is expected to have a
95
+ # gemspec. Standard library and application files are not gems,
96
+ # and reporting each of them buried real errors under hundreds
97
+ # of lines.
98
+ puts "Failed to load gemspec for #{feature}"
95
99
  end
96
100
 
97
101
  gems
data/lib/ocran/option.rb CHANGED
@@ -886,5 +886,45 @@ EOF
886
886
  return false unless Gem.win_platform?
887
887
  @options[:windowed?]
888
888
  end
889
+
890
+ # Where the compiled launcher stubs live (StubBuilder::STUB_PATH and
891
+ # STUBW_PATH). A released gem ships them; a git checkout, such as
892
+ # Bundler's `gem "ocran", git: ...`, has only their sources in src/.
893
+ STUB_DIR = File.expand_path("../../share/ocran", __dir__)
894
+
895
+ # The prebuilt stub the build will copy, or nil when it copies none:
896
+ # native compilation, --wasm, --static-runtime and --cosmo-ruby's ZIP
897
+ # mode use no stub, --cosmo compiles its own, and installer, directory
898
+ # and zip output only need one for the wrapper executable.
899
+ def prebuilt_stub
900
+ return nil if spinel? || roundhouse? || wasm_runtime || static_runtime? || cosmo_zip? || cosmo_cc
901
+ return nil if (inno_setup_script || output_dir || output_zip) && !wrapper_exe?
902
+
903
+ name = if !Gem.win_platform? then "stub"
904
+ elsif windowed? && !macosx_bundle then "stubw.exe"
905
+ else "stub.exe"
906
+ end
907
+ File.join(STUB_DIR, name)
908
+ end
909
+
910
+ # Fails when the build needs a stub that was never compiled. Checked
911
+ # before the dependency run, which can take minutes, since the build
912
+ # would otherwise end in a bare ENOENT for the stub after it.
913
+ def check_prebuilt_stub(path = prebuilt_stub)
914
+ return if path.nil? || File.exist?(path)
915
+
916
+ root = File.dirname(STUB_DIR, 2)
917
+ build = if RUBY_PLATFORM.include?("mswin")
918
+ "`nmake -f Makefile.mswin install` in #{File.join(root, "src")} from a Visual Studio developer prompt"
919
+ elsif Gem.win_platform?
920
+ "`ridk exec make -C src install` in #{root}"
921
+ else
922
+ "`make -C src install` in #{root}"
923
+ end
924
+ message = "The launcher stub #{path} has not been built (OCRAN from a git checkout " \
925
+ "ships only its sources): run #{build}"
926
+ message += ", or pass --no-wrapper-exe to skip the wrapper executable" if inno_setup_script || output_dir || output_zip
927
+ raise message
928
+ end
889
929
  end
890
930
  end
data/lib/ocran/runner.rb CHANGED
@@ -29,6 +29,7 @@ module Ocran
29
29
  load File.expand_path("option.rb", __dir__)
30
30
  @option = Option.new.tap do |opt|
31
31
  opt.parse(ARGV)
32
+ opt.check_prebuilt_stub
32
33
  rescue *USER_ERRORS => e
33
34
  # Invalid option arguments, usually; report them as such.
34
35
  user_error e
@@ -207,6 +208,14 @@ module Ocran
207
208
  # environment, so it is preferable to ignore exceptions other than
208
209
  # critical errors.
209
210
  warning "#{mod}::#{const} loading failed: #{e.message}"
211
+ rescue SystemExit => e
212
+ # Some libraries exit when an optional dependency is missing -
213
+ # rdoc/servlet.rb aborts without webrick, which Ruby 3.4 no
214
+ # longer ships. That ended the whole build (exit 1) over a
215
+ # constant the application never asked for; the abort message
216
+ # went to stderr ahead of the buffered progress output.
217
+ warning "#{mod}::#{const} loading exited (status #{e.status})" \
218
+ "#{e.message == "exit" ? "" : ": #{e.message}"}"
210
219
  end
211
220
  end
212
221
  checked_modules[mod] = true
@@ -188,6 +188,10 @@ module Ocran
188
188
  cmd = cmd.map { |a| as_dll.fetch(a, a) }
189
189
  cmd += ["-o", output.to_s] unless args.any? { |a| a.include?("@OUTPUT@") }
190
190
  cmd += ["-Wl,-sectcreate,__OCRAN,__zip,#{archive}"] if archive
191
+ # macOS: no debug map (the objects' paths, here in a temporary
192
+ # directory, would differ every time), and nothing for strip to do,
193
+ # which would break the signature the linker writes.
194
+ cmd += ["-Wl,-S"] if @info["target_os"].to_s.start_with?("darwin")
191
195
  run(*cmd)
192
196
  end
193
197
  strip = strip_tool
@@ -242,8 +246,11 @@ module Ocran
242
246
  # Compilers, linkers and strip run with SOURCE_DATE_EPOCH set: binutils
243
247
  # then writes that, not the current time, into a PE header it rewrites
244
248
  # (strip does), so that the same application links to the same bytes.
249
+ # ZERO_AR_DATE does the same for Apple's ld, which otherwise writes the
250
+ # objects' modification times into the executable's debug map.
245
251
  def run(*cmd)
246
- out, status = Open3.capture2e({ "SOURCE_DATE_EPOCH" => ENV["SOURCE_DATE_EPOCH"] || "1767225600" }, *cmd)
252
+ env = { "SOURCE_DATE_EPOCH" => ENV["SOURCE_DATE_EPOCH"] || "1767225600", "ZERO_AR_DATE" => "1" }
253
+ out, status = Open3.capture2e(env, *cmd)
247
254
  raise "#{cmd.first} failed:\n#{out.lines.last(25).join}" unless status.success?
248
255
 
249
256
  out
data/lib/ocran/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ocran
4
- VERSION = "1.5.0"
4
+ VERSION = "1.5.1"
5
5
  end
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.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andi Idogawa
@@ -59,6 +59,7 @@ files:
59
59
  - lib/ocran/ed_resource.rb
60
60
  - lib/ocran/ed_resource_builder.rb
61
61
  - lib/ocran/empty_source
62
+ - lib/ocran/env_hook.rb
62
63
  - lib/ocran/file_path_set.rb
63
64
  - lib/ocran/gem_spec_queryable.rb
64
65
  - lib/ocran/host_config_helper.rb
@@ -120,6 +121,8 @@ metadata:
120
121
  homepage_uri: https://github.com/largo/ocran
121
122
  source_code_uri: https://github.com/largo/ocran
122
123
  changelog_uri: https://github.com/largo/ocran/blob/master/CHANGELOG.txt
124
+ bug_tracker_uri: https://github.com/largo/ocran/issues
125
+ documentation_uri: https://github.com/largo/ocran#readme
123
126
  rdoc_options: []
124
127
  require_paths:
125
128
  - lib