prebake 0.2.10 → 0.3.0

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: 5016083674e522c23d95fa0526df6b35f159e529b0d3116fc24daf70e1c52df9
4
- data.tar.gz: 7a32973b781cb342998d150c5fe75b944c4865055df2f168c2bebeb48fa1450c
3
+ metadata.gz: 27e9af4988afacc2c07ae6e09eac8aa9efa26d44dc95401cd4f1d52e14993bd6
4
+ data.tar.gz: 801beefc4a2eed88cda591e6cf0b6abbef13a8f597f185eececeafd49b9e6857
5
5
  SHA512:
6
- metadata.gz: b9ea7f985f4ca767edaca8598a79023e0ec9c0fe1ea9e5219a368ce4d44e4d0ef8ccb8bf8a57434fce514afc9e80694a4a72c3e2335d54ba37d2857a8d43476d
7
- data.tar.gz: 05adfe955164c50037842cd380504e938df4618160de34c9fee7e7e25e225226eefb44a4a9fc6a2181f41702e23ff2ed00686bffaee20643de811cde9d98922a
6
+ metadata.gz: 9911d705b900209619e8051a3d01acb4fdd1337b026f93b577f6b6113cee8ce0a18fa5f88af9316988c7433f01232700731776ff562b8f7d85910cdcd94c802f
7
+ data.tar.gz: 80a33006c4a8159bb0b5f594b95c9b60f443e0126f3862fc2c526424794bc2acf6ff2c90ad4bbbec88c58ce0a1dba40ff05c7cc48001e9c0568675f53013fc07
@@ -75,6 +75,12 @@ module Prebake
75
75
  end
76
76
  end
77
77
 
78
+ if !Prebake.libruby_available? && ElfInspector.libruby_needed_for_gem?(gem_path)
79
+ Logger.warn "Skipping push of #{cache_key}: binary requires libruby.so (dynamic Ruby) but this is a static Ruby build; binary would crash on this platform"
80
+ FileUtils.rm_f(gem_path)
81
+ return nil
82
+ end
83
+
78
84
  Logger.debug "Built #{cache_key}"
79
85
  [gem_path, cache_key, checksum, backend]
80
86
  rescue StandardError => e
@@ -9,17 +9,9 @@ require_relative "logger"
9
9
  module Prebake
10
10
  module ElfInspector
11
11
  def self.required_glibc_for_gem(gem_path)
12
- Dir.mktmpdir("prebake-portability") do |tmpdir|
13
- Gem::Package.new(gem_path).extract_files(tmpdir)
14
- versions = Dir.glob(File.join(tmpdir, "**/*.{so,bundle,dll}")).filter_map do |binary|
15
- next if File.symlink?(binary) || File.size(binary).zero?
16
-
17
- required_glibc(binary)
18
- end
19
- return nil if versions.empty?
20
-
21
- versions.max_by { |v| Gem::Version.new(v) }
22
- end
12
+ versions = []
13
+ each_gem_binary(gem_path) { |binary| v = required_glibc(binary); versions << v if v }
14
+ versions.empty? ? nil : versions.max_by { |v| Gem::Version.new(v) }
23
15
  rescue StandardError => e
24
16
  # Malformed gem, missing objdump, or I/O error — treat as unknown, let
25
17
  # downstream extraction catch real corruption.
@@ -43,6 +35,27 @@ module Prebake
43
35
  versions.max_by { |v| Gem::Version.new(v) }
44
36
  end
45
37
 
38
+ def self.libruby_needed_for_gem?(gem_path)
39
+ each_gem_binary(gem_path) { |binary| return true if libruby_needed?(binary) }
40
+ false
41
+ rescue StandardError => e
42
+ Logger.debug "libruby inspection failed for #{File.basename(gem_path)}: #{e.message}"
43
+ false
44
+ end
45
+
46
+ def self.libruby_needed?(path)
47
+ needed_libraries(path).any? { |lib| lib.start_with?("libruby") }
48
+ end
49
+
50
+ def self.needed_libraries(path)
51
+ out, status = Open3.capture2e("objdump", "-p", path)
52
+ return [] unless status.success?
53
+
54
+ out.scan(/NEEDED\s+(\S+)/).flatten
55
+ rescue Errno::ENOENT
56
+ []
57
+ end
58
+
46
59
  def self.run_objdump(path)
47
60
  out, status = Open3.capture2e("objdump", "-T", path)
48
61
  return nil unless status.success?
@@ -51,5 +64,16 @@ module Prebake
51
64
  rescue Errno::ENOENT
52
65
  nil
53
66
  end
67
+
68
+ private_class_method def self.each_gem_binary(gem_path)
69
+ Dir.mktmpdir("prebake-gem") do |tmpdir|
70
+ Gem::Package.new(gem_path).extract_files(tmpdir)
71
+ Dir.glob(File.join(tmpdir, "**/*.{so,bundle,dll}")).each do |binary|
72
+ next if File.symlink?(binary) || File.size(binary).zero?
73
+
74
+ yield binary
75
+ end
76
+ end
77
+ end
54
78
  end
55
79
  end
@@ -6,8 +6,7 @@ require "digest"
6
6
  require_relative "cache_key"
7
7
  require_relative "platform"
8
8
  require_relative "extractor"
9
- require_relative "elf_inspector"
10
- require_relative "glibc"
9
+ require_relative "portability_guard"
11
10
  require_relative "logger"
12
11
 
13
12
  module Prebake
@@ -17,6 +16,12 @@ module Prebake
17
16
  return super unless Prebake.enabled?
18
17
  return super unless Prebake.backend # nil if config failed
19
18
 
19
+ if Prebake.optional_native_extension?(@spec.name) && !Prebake.libruby_available?
20
+ Logger.warn "#{@spec.name}: native extension skipped (optional gem, libruby.so absent on static Ruby build)"
21
+ install_without_native_extension
22
+ return
23
+ end
24
+
20
25
  platform = Platform.generalized
21
26
  cache_key = CacheKey.for(@spec.name, @spec.version.to_s, platform)
22
27
 
@@ -40,7 +45,7 @@ module Prebake
40
45
  end
41
46
 
42
47
  if verify_checksum(cache_key, expected_checksum, cached_gem)
43
- unless portable_for_host?(cached_gem)
48
+ unless PortabilityGuard.portable_for_host?(cached_gem, spec_name: @spec.name)
44
49
  # Binary is valid for other hosts; don't delete from backend.
45
50
  return super
46
51
  end
@@ -85,16 +90,8 @@ module Prebake
85
90
  end
86
91
  end
87
92
 
88
- def portable_for_host?(gem_path)
89
- # Cache key already segregates platforms; glibc check only applies on linux.
90
- return true unless Glibc.linux?
91
- return true if Prebake.skip_portability_check?
92
-
93
- required = ElfInspector.required_glibc_for_gem(gem_path)
94
- return true if Glibc.compatible?(required)
95
-
96
- Logger.warn "Cached #{@spec.name} requires glibc #{required}, host has #{Glibc.detected_version || 'unknown'}; falling back to source build"
97
- false
93
+ def install_without_native_extension
94
+ mark_build_complete
98
95
  end
99
96
 
100
97
  def install_from_cache(gem_path)
@@ -105,9 +102,13 @@ module Prebake
105
102
  return false
106
103
  end
107
104
 
105
+ mark_build_complete
106
+ true
107
+ end
108
+
109
+ def mark_build_complete
108
110
  FileUtils.mkdir_p(File.dirname(@spec.gem_build_complete_path))
109
111
  FileUtils.touch(@spec.gem_build_complete_path)
110
- true
111
112
  end
112
113
  end
113
114
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "elf_inspector"
4
+ require_relative "glibc"
5
+ require_relative "logger"
6
+
7
+ module Prebake
8
+ # Host-side portability guards for cached gems: ensures the cached binary
9
+ # will actually load on this host (glibc version + libruby.so presence).
10
+ module PortabilityGuard
11
+ module_function
12
+
13
+ def portable_for_host?(gem_path, spec_name:)
14
+ return true unless Glibc.linux?
15
+ return true if Prebake.skip_portability_check?
16
+
17
+ glibc_ok?(gem_path, spec_name:) && libruby_ok?(gem_path, spec_name:)
18
+ end
19
+
20
+ def glibc_ok?(gem_path, spec_name:)
21
+ required = ElfInspector.required_glibc_for_gem(gem_path)
22
+ return true if Glibc.compatible?(required)
23
+
24
+ Logger.warn "Cached #{spec_name} requires glibc #{required}, " \
25
+ "host has #{Glibc.detected_version || 'unknown'}; falling back to source build"
26
+ false
27
+ end
28
+
29
+ def libruby_ok?(gem_path, spec_name:)
30
+ return true if Prebake.libruby_available?
31
+ return true unless ElfInspector.libruby_needed_for_gem?(gem_path)
32
+
33
+ Logger.warn "Cached #{spec_name} requires libruby.so (dynamic Ruby build) " \
34
+ "but this host has a static Ruby; falling back to source build"
35
+ false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prebake
4
+ # Detection and configuration for static Ruby builds (e.g. Paketo MRI buildpack),
5
+ # where libruby.so is absent and native extensions dynamically linked against it
6
+ # would crash at load time.
7
+ module StaticRuby
8
+ # Gems whose native extensions are entirely optional — the gem runs correctly in pure Ruby
9
+ # mode when the extension can't be loaded. Prebake skips the extension for these gems
10
+ # instead of installing a broken .so. Extend via PREBAKE_OPTIONAL_NATIVE_EXTENSIONS=gem1,gem2.
11
+ OPTIONAL_NATIVE_EXTENSIONS_DEFAULT = %w[bootsnap].freeze
12
+
13
+ class << self
14
+ def optional_native_extensions
15
+ @optional_native_extensions ||= begin
16
+ extra = ENV.fetch("PREBAKE_OPTIONAL_NATIVE_EXTENSIONS", "")
17
+ .split(",").map(&:strip).reject(&:empty?)
18
+ (OPTIONAL_NATIVE_EXTENSIONS_DEFAULT + extra).uniq
19
+ end
20
+ end
21
+
22
+ def optional_native_extension?(gem_name)
23
+ optional_native_extensions.include?(gem_name)
24
+ end
25
+
26
+ def libruby_available?
27
+ return @libruby_available if defined?(@libruby_available)
28
+
29
+ libruby_so = RbConfig::CONFIG["LIBRUBY_SO"]
30
+ @libruby_available = !libruby_so.nil? && !libruby_so.empty? &&
31
+ File.exist?(File.join(RbConfig::CONFIG["libdir"], libruby_so))
32
+ end
33
+
34
+ def reset!
35
+ remove_instance_variable(:@optional_native_extensions) if defined?(@optional_native_extensions)
36
+ remove_instance_variable(:@libruby_available) if defined?(@libruby_available)
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/prebake.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "prebake/logger"
4
+ require_relative "prebake/static_ruby"
4
5
 
5
6
  module Prebake
6
7
  class Error < StandardError; end
@@ -27,6 +28,10 @@ module Prebake
27
28
  ENV.fetch("PREBAKE_MAX_GLIBC", nil)
28
29
  end
29
30
 
31
+ def self.optional_native_extensions = StaticRuby.optional_native_extensions
32
+ def self.optional_native_extension?(gem_name) = StaticRuby.optional_native_extension?(gem_name)
33
+ def self.libruby_available? = StaticRuby.libruby_available?
34
+
30
35
  def self.backend
31
36
  return @backend if defined?(@backend_loaded)
32
37
 
@@ -49,6 +54,7 @@ module Prebake
49
54
  def self.reset!
50
55
  remove_instance_variable(:@backend_loaded) if defined?(@backend_loaded)
51
56
  remove_instance_variable(:@backend) if defined?(@backend)
57
+ StaticRuby.reset!
52
58
  end
53
59
 
54
60
  def self.setup!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prebake
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.10
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thejus Paul
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-20 00:00:00.000000000 Z
11
+ date: 2026-04-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Prebake speeds up bundle install by skipping native gem compilation.
14
14
  It fetches precompiled binaries for gems like puma, nokogiri, pg, grpc, and bootsnap
@@ -40,6 +40,8 @@ files:
40
40
  - lib/prebake/logger.rb
41
41
  - lib/prebake/platform.rb
42
42
  - lib/prebake/platform_gem_builder.rb
43
+ - lib/prebake/portability_guard.rb
44
+ - lib/prebake/static_ruby.rb
43
45
  - plugins.rb
44
46
  homepage: https://github.com/gembakery/prebake
45
47
  licenses: