ocran 1.4.5 → 1.4.6

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: 85cce39e5b93c48ccd16ecc3ad73f214a2349c2b203ff242fb1d478a78fbd5b9
4
- data.tar.gz: f6ac990c20617164a68d70cd91e38e42afd12889c247f82204684d1843bb26ae
3
+ metadata.gz: 9898f2fb5230aa7ee4f9bec0b47da2609bed63b37042b7ccb2b0dce9a694f25b
4
+ data.tar.gz: a3ae88ab734250742900487fa9a4dd3b862da930f11c9f656959b197efc7d24c
5
5
  SHA512:
6
- metadata.gz: 598cfd6fa803707a732ec8c10687f221b2a0618570377655a7414bcb94d9744d59364ade26ddc5b379fd72daf40adf8d001eccdac3d7af0ab20a69dfbafe1500
7
- data.tar.gz: 83311e9e259a7c91f99ab6860529bd3ede2d706b3737451707f7488291679ae0ceb49bddb110128b6413f7d99b38f4b06bdd0a2362acf56a0afb9269bb41ae82
6
+ metadata.gz: 94b73edf3033cbda84a6d8f5d446b23acdffac467ac06c2881821c3caccb479bf5673e69662c96735d17cc2bfc0db43d0b44a0613e094c1e4251eac3cf915919
7
+ data.tar.gz: a6fb23433b4fca4ed3379a8486b6202a7c1c0272799034ee28543ab8c137f7d3fd8b04ad23f1a87f2012f98ec65505297e6bc51a90247426676170a1cc909151
data/CHANGELOG.txt CHANGED
@@ -1,3 +1,9 @@
1
+ === 1.4.6
2
+ - Applications no longer carry gems that only OCRAN itself activated. The gem scan read Gem.loaded_specs while the build was already running instead of the snapshot taken after the dependency run, so whatever OCRAN had activated by then was packed as if the application had loaded it: OCRAN's own gem and its fiddle dependency, activated by the `ocran` command of an installed OCRAN (since OCRA 1.3.2, which started packing every gem in Gem.loaded_specs; about 390 KB of stubs, lzma.exe and sources per application), and since 1.3.16, whose refactoring moved the build ahead of the gem scan, also tempfile, tmpdir, fileutils, delegate and etc from the stub builder and fiddle from DLL detection. The scan now uses the snapshot and leaves out the running OCRAN's gem and its runtime dependencies. Only activation is discounted: a gem the application does load is still packed. The same snapshot decides which gems' SxS assembly manifests are included on Windows.
3
+ - Gems that Bundler installed into a configured path are found without Bundler (issue #61). ruby/setup-ruby with `bundler-cache: true` - the README's GitHub Actions example - installs the Gemfile's gems into vendor/bundle, a place plain RubyGems never looks, so `ocran app.rb` died with a LoadError on the first gem during the dependency run, `--gemfile` included. Before loading the script OCRAN now asks Bundler, in a child process and without evaluating the Gemfile, where the application's bundle is installed, and adds that directory to the gem path when it is not the system gems. The bundle is not set up, so gems the Gemfile does not list stay loadable as before; a bundle installed into the system gems and `bundle exec` are unaffected.
4
+ - On Windows, an `--output` name without an extension now gets .exe (names ending in .exe or .com are used as given). The name used to be taken verbatim, so `ocran myapp.rb --output myapp` - the README's GitHub Actions example, whose comment promised myapp.exe - produced an extensionless `myapp` that Windows does not run. Other platforms keep the name as given. New CI workflow test-readme-actions.yml runs that example step for step on Windows, Linux and both macOS architectures against OCRAN from the commit under test.
5
+ - Repository: removed a Ruby 3.3 bundle (vendor/bundle, 4,586 files), .bundle/config and a stray .ape-1.10 binary that a merge commit had checked in on 2026-08-09, and ignore them from now on. The bundle's binstubs call `ruby3.3`, which CI runners do not have, so every Ruby 3.3 test job failed before running a test once the bundler cache no longer replaced them; the jobs' cache-version is bumped to drop the caches that restored them. None of it was ever part of a published gem.
6
+
1
7
  === 1.4.5
2
8
  - Windows: detected DLLs are now also bundled into bin, next to ruby.exe, whenever they are not already resolvable from where they get packed. The Windows loader resolves a native extension's imports from the extension's own directory, ruby.exe's application directory (bin) plus its ruby_builtin_dlls SxS assembly, and the system directories - PATH is not consulted on hardened systems, and the AddDllDirectory route gems take through ruby_installer/runtime does not exist in a packed app. A DLL loaded from a gem's own tree (e.g. FreeTDS, which tiny_tds ships under ports/), from a devkit's msys64 tree inside the Ruby prefix, or from outside the prefix entirely was packed only at that original location, which the loader never searches, so the packaged application died at require time with a misleading LoadError on machines where a rich PATH did not mask the gap - the out-of-prefix case was skipped entirely by a guard that made its copy_to_bin branch unreachable. DLLs from the Windows directory keep coming from the target system and are never bundled. Companion DLLs found next to native extensions (e.g. libssl-3-x64.dll beside openssl.so in archdir) go into bin as well: a copy in archdir only helps extensions in archdir itself, while the same extension packed at a gem path (openssl and psych are gems since Ruby 3.x) resolves its imports from bin.
3
9
  - `--cosmo-ruby`: a native gem now also counts as provided by the payload when the payload can resolve the gem's primary feature, not only when the payload has a gemspec of that name. A gemspec is not what makes a library requirable - an extension linked into the APE, or a library in the interpreter's embedded stdlib, answers `require` with nothing under /zip/lib/ruby/gems/*/specifications - so gems such as cgi and pathname (both compiled into CosmoRuby, and both ordinary native gems on Ruby 3.4+) were reported incompatible and refused builds that work. The probe runs the payload once using $LOAD_PATH.resolve_feature_path, which searches exactly as require does, built-in extensions included, but executes none of the code it finds.
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # OCRAN
2
2
 
3
+ ![OCRAN: ship your Ruby app as one file, no Ruby needed to run it](docs/social/twitter-card.png)
4
+
3
5
  [OCRAN (One-Click Ruby Application Next)](https://github.com/largo/ocran) packages Ruby applications for
4
6
  distribution. It bundles your script, the Ruby interpreter, gems, and native
5
7
  libraries into a single self-contained artifact that runs without requiring
@@ -191,8 +191,7 @@ module Ocran
191
191
  end
192
192
  end
193
193
  if defined?(Gem)
194
- foreign = foreign_bundle_gem_names
195
- specs += Gem.loaded_specs.each_value.reject { |spec| foreign.include?(spec.name) }
194
+ specs += application_loaded_specs
196
195
  # Now, we also detect gems that are not included in Gem.loaded_specs.
197
196
  # Therefore, we look for any loaded file from a gem path.
198
197
  specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
@@ -202,6 +201,46 @@ module Ocran
202
201
  specs
203
202
  end
204
203
 
204
+ # The gems RubyGems had activated when the dependency run ended, less
205
+ # those that are not the application's.
206
+ #
207
+ # Read from the snapshot taken with the loaded features, not from
208
+ # Gem.loaded_specs, which by now also holds what OCRAN activated for the
209
+ # build itself - fiddle for DLL detection, tempfile with tmpdir,
210
+ # fileutils, delegate and etc for the builder - and which every
211
+ # application used to get packed since 1.3.16 moved the build ahead of
212
+ # the gem scan. OCRAN's own gem and its runtime dependencies (fiddle) are
213
+ # dropped as well: the `ocran` command activates them before the
214
+ # dependency run starts, and since OCRA 1.3.2 they were packed into
215
+ # every application built from an installed OCRAN, although nothing in a
216
+ # packaged application loads OCRAN. Only activation is discounted: an
217
+ # application that does load one of them still gets it packed through
218
+ # detect_gems_from.
219
+ def application_loaded_specs
220
+ @application_loaded_specs ||= begin
221
+ excluded = foreign_bundle_gem_names | ocran_gem_names
222
+ @post_env.loaded_specs.reject { |spec| excluded.include?(spec.name) }
223
+ end
224
+ end
225
+
226
+ # The names of the gem this OCRAN runs from and of the gems it activated
227
+ # as its runtime dependencies. Empty when OCRAN does not run from an
228
+ # installed gem, as from a source checkout.
229
+ def ocran_gem_names
230
+ specs = @post_env.loaded_specs.to_h { |spec| [spec.name, spec] }
231
+ own = specs.each_value.find { |spec| Pathname(__dir__).subpath?(spec.gem_dir) }
232
+ names = Set.new
233
+ queue = own ? [own] : []
234
+ while (spec = queue.shift)
235
+ next unless names.add?(spec.name)
236
+
237
+ spec.runtime_dependencies.each do |dep|
238
+ queue << specs[dep.name] if specs.key?(dep.name)
239
+ end
240
+ end
241
+ names
242
+ end
243
+
205
244
  # The gems RubyGems had already activated for a bundle that is not the
206
245
  # application's, by the time OCRAN started.
207
246
  #
@@ -560,7 +599,7 @@ module Ocran
560
599
  archdir = Pathname(RbConfig::CONFIG["archdir"])
561
600
  sxs_manifest_dirs << archdir if archdir.exist? && archdir.subpath?(exec_prefix)
562
601
  if defined?(Gem)
563
- Gem.loaded_specs.each_value do |spec|
602
+ application_loaded_specs.each do |spec|
564
603
  next if spec.extensions.empty?
565
604
  ext_dir = Pathname(spec.extension_dir)
566
605
  sxs_manifest_dirs << ext_dir if ext_dir.exist? && ext_dir.subpath?(exec_prefix)
data/lib/ocran/option.rb CHANGED
@@ -277,7 +277,15 @@ EOF
277
277
 
278
278
  @options[:output_executable] =
279
279
  if output_override
280
- output_override
280
+ # Windows runs a program only by its extension, so a name given
281
+ # without .exe - `--output myapp`, as a build script shared with
282
+ # Linux and macOS writes it - gets one there. .exe and .com names
283
+ # are used as given.
284
+ if Gem.win_platform? && !output_override.extname?(".exe") && !output_override.extname?(".com")
285
+ Pathname("#{output_override}.exe")
286
+ else
287
+ output_override
288
+ end
281
289
  else
282
290
  executable = script
283
291
  # If debug mode is enabled, append "-debug" to the filename
data/lib/ocran/runner.rb CHANGED
@@ -49,6 +49,7 @@ module Ocran
49
49
  else
50
50
  warn_about_foreign_bundle
51
51
  end
52
+ expose_bundle_install_path
52
53
  say "Loading script to check dependencies"
53
54
  $PROGRAM_NAME = @option.script.to_s
54
55
  end
@@ -110,6 +111,58 @@ module Ocran
110
111
  (app_gemfile ? "; pass --gemfile #{app_gemfile} to package against that bundle" : "")
111
112
  end
112
113
 
114
+ # `bundle install` puts the gems where Bundler is configured to put them,
115
+ # and a configured path - `bundle config set path vendor/bundle`, which is
116
+ # what ruby/setup-ruby's `bundler-cache: true` does - is a place only
117
+ # Bundler looks. The dependency run loads the script as plain Ruby, so
118
+ # its first `require` of a gem from such a bundle raised LoadError
119
+ # although the bundle was installed (github issue #61).
120
+ #
121
+ # Add the bundle's install directory to the gem path. Its gems become
122
+ # visible exactly as if they had been installed into Ruby's own gem
123
+ # directory, and nothing more: setting the bundle up would also hide
124
+ # every gem the Gemfile does not list, breaking scripts that work today.
125
+ # A bundle installed into the system gems, the ordinary case outside CI,
126
+ # changes nothing, and under `bundle exec` Bundler has already put its
127
+ # gems on the gem path.
128
+ def expose_bundle_install_path
129
+ return if @pre_env.bundler_setup_loaded?
130
+
131
+ gemfile = @option.application_gemfile or return
132
+ path = bundle_install_path(gemfile) or return
133
+ return if Gem.path.any? { |gem_path| same_file?(gem_path, path) }
134
+
135
+ verbose "Adding #{path}, where the bundle of #{gemfile} is installed, to the gem path"
136
+ Gem.paths = { "GEM_HOME" => Gem.dir, "GEM_PATH" => (Gem.path + [path]).join(File::PATH_SEPARATOR) }
137
+ end
138
+
139
+ # Ruby run in a child process to print where Bundler installs the bundle
140
+ # of BUNDLE_GEMFILE, or nothing when that is the system gems.
141
+ BUNDLE_PATH_QUERY = <<~RUBY
142
+ require "bundler"
143
+ print Bundler.bundle_path unless Bundler.configured_bundle_path.use_system_gems?
144
+ RUBY
145
+ private_constant :BUNDLE_PATH_QUERY
146
+
147
+ # The directory the bundle of the given Gemfile is installed into when
148
+ # that is not the system gems, or nil. Bundler decides it from its
149
+ # settings - .bundle/config beside the Gemfile, the user's configuration
150
+ # and BUNDLE_* variables - without evaluating the Gemfile.
151
+ #
152
+ # Asked in a child process: loading Bundler here, before the dependency
153
+ # run, would put Bundler's files among the script's loaded features and
154
+ # pack them into every application with a Gemfile.
155
+ def bundle_install_path(gemfile)
156
+ env = { "BUNDLE_GEMFILE" => gemfile.to_s, "RUBYOPT" => nil, "BUNDLER_SETUP" => nil }
157
+ path = IO.popen(env, [RbConfig.ruby, "-e", BUNDLE_PATH_QUERY], err: File::NULL, &:read)
158
+ return nil unless $?.success? && !path.empty? && File.directory?(path)
159
+
160
+ path
161
+ rescue SystemCallError => e
162
+ verbose "Could not ask Bundler where the bundle of #{gemfile} is installed: #{e.message}"
163
+ nil
164
+ end
165
+
113
166
  # The Gemfile this process was started against, if any.
114
167
  def active_bundler_gemfile
115
168
  return ENV["BUNDLE_GEMFILE"] unless ENV["BUNDLE_GEMFILE"].to_s.empty?
@@ -15,18 +15,26 @@ module Ocran
15
15
  # under.
16
16
  BUNDLER_SETUP_FEATURE = %r{[\\/]bundler[\\/]setup\.rb\z}
17
17
 
18
- attr_reader :env, :load_path, :loaded_features, :pwd, :activated_gems
18
+ attr_reader :env, :load_path, :loaded_features, :pwd, :loaded_specs
19
19
 
20
20
  def initialize
21
21
  @env = ENV.to_hash.freeze
22
22
  @load_path = $LOAD_PATH.dup.freeze
23
23
  @loaded_features = $LOADED_FEATURES.dup.freeze
24
24
  @pwd = Dir.pwd.freeze
25
- # The gems RubyGems had activated at this point. Under `bundle exec`
26
- # that is the entire bundle, activated before OCRAN's first line runs,
27
- # so a snapshot taken before the dependency run is what tells the build
28
- # environment's gems apart from the application's.
29
- @activated_gems = (defined?(Gem) ? Gem.loaded_specs.keys : []).freeze
25
+ # The specs of the gems RubyGems had activated at this point. Taken
26
+ # with the loaded features for the same reason: OCRAN goes on to
27
+ # activate gems of its own while it builds (fiddle for DLL detection,
28
+ # tempfile for the builder, ...), and those are not the application's.
29
+ # Under `bundle exec` the snapshot taken before the dependency run is
30
+ # the entire bundle, activated before OCRAN's first line runs, which is
31
+ # what tells the build environment's gems apart from the application's.
32
+ @loaded_specs = (defined?(Gem) ? Gem.loaded_specs.values : []).freeze
33
+ end
34
+
35
+ # The names of the gems in #loaded_specs.
36
+ def activated_gems
37
+ @loaded_specs.map(&:name)
30
38
  end
31
39
 
32
40
  # Whether Bundler had already set this process up when the snapshot was
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.4.5"
4
+ VERSION = "1.4.6"
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.4.5
4
+ version: 1.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andi Idogawa
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  requirements: []
119
- rubygems_version: 4.0.16
119
+ rubygems_version: 4.0.20
120
120
  specification_version: 4
121
121
  summary: OCRAN (One-Click Ruby Application Next) packages Ruby applications for distribution
122
122
  on Windows, Linux, and macOS.