prebake 0.2.9 → 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: b2c8911f4634bbddf8455aae1f54efc5985f72372a3cd1581fa890da5fc7e543
4
- data.tar.gz: 2efa89bd847b4c67eceb11479452267bd68812e503bba51f997317e469cd8b38
3
+ metadata.gz: 27e9af4988afacc2c07ae6e09eac8aa9efa26d44dc95401cd4f1d52e14993bd6
4
+ data.tar.gz: 801beefc4a2eed88cda591e6cf0b6abbef13a8f597f185eececeafd49b9e6857
5
5
  SHA512:
6
- metadata.gz: 73e53914ed4dec0e1a0b5ce3a9be292cd75cf0d8e4c24336ad341b0573f939e46c09f079e2afde4b23ba51fd1127aaa54dae96018546492187417689b5ccdefe
7
- data.tar.gz: ed0b096a62e01f40d859033457ed6ed0b9ba8301df0fa90bed67100195687afe33af3e92a52959d8c2bfbbc741dba9be7b3994def8f998a23641fb14eca1d2be
6
+ metadata.gz: 9911d705b900209619e8051a3d01acb4fdd1337b026f93b577f6b6113cee8ce0a18fa5f88af9316988c7433f01232700731776ff562b8f7d85910cdcd94c802f
7
+ data.tar.gz: 80a33006c4a8159bb0b5f594b95c9b60f443e0126f3862fc2c526424794bc2acf6ff2c90ad4bbbec88c58ce0a1dba40ff05c7cc48001e9c0568675f53013fc07
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require_relative "platform_gem_builder"
5
5
  require_relative "cache_key"
6
6
  require_relative "platform"
7
+ require_relative "elf_inspector"
7
8
  require_relative "logger"
8
9
 
9
10
  module Prebake
@@ -65,6 +66,21 @@ module Prebake
65
66
  gem_path = builder.build
66
67
  checksum = builder.checksum
67
68
 
69
+ if (max = Prebake.max_glibc)
70
+ required = ElfInspector.required_glibc_for_gem(gem_path)
71
+ if required && Gem::Version.new(required) > Gem::Version.new(max)
72
+ Logger.warn "Skipping push of #{cache_key}: requires glibc #{required} (> PREBAKE_MAX_GLIBC=#{max})"
73
+ FileUtils.rm_f(gem_path)
74
+ return nil
75
+ end
76
+ end
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
+
68
84
  Logger.debug "Built #{cache_key}"
69
85
  [gem_path, cache_key, checksum, backend]
70
86
  rescue StandardError => e
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "tempfile"
5
+ require "tmpdir"
6
+ require "rubygems/package"
7
+ require_relative "logger"
8
+
9
+ module Prebake
10
+ module ElfInspector
11
+ def self.required_glibc_for_gem(gem_path)
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) }
15
+ rescue StandardError => e
16
+ # Malformed gem, missing objdump, or I/O error — treat as unknown, let
17
+ # downstream extraction catch real corruption.
18
+ Logger.debug "Portability inspection failed for #{File.basename(gem_path)}: #{e.message}"
19
+ nil
20
+ end
21
+
22
+ def self.required_glibc(path)
23
+ return nil unless File.exist?(path)
24
+
25
+ output = run_objdump(path)
26
+ return nil if output.nil? || output.empty?
27
+
28
+ parse_glibc_version(output)
29
+ end
30
+
31
+ def self.parse_glibc_version(output)
32
+ versions = output.scan(/GLIBC_(\d+(?:\.\d+)+)/).flatten
33
+ return nil if versions.empty?
34
+
35
+ versions.max_by { |v| Gem::Version.new(v) }
36
+ end
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
+
59
+ def self.run_objdump(path)
60
+ out, status = Open3.capture2e("objdump", "-T", path)
61
+ return nil unless status.success?
62
+
63
+ out
64
+ rescue Errno::ENOENT
65
+ nil
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
78
+ end
79
+ end
@@ -6,6 +6,7 @@ require "digest"
6
6
  require_relative "cache_key"
7
7
  require_relative "platform"
8
8
  require_relative "extractor"
9
+ require_relative "portability_guard"
9
10
  require_relative "logger"
10
11
 
11
12
  module Prebake
@@ -15,6 +16,12 @@ module Prebake
15
16
  return super unless Prebake.enabled?
16
17
  return super unless Prebake.backend # nil if config failed
17
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
+
18
25
  platform = Platform.generalized
19
26
  cache_key = CacheKey.for(@spec.name, @spec.version.to_s, platform)
20
27
 
@@ -38,6 +45,11 @@ module Prebake
38
45
  end
39
46
 
40
47
  if verify_checksum(cache_key, expected_checksum, cached_gem)
48
+ unless PortabilityGuard.portable_for_host?(cached_gem, spec_name: @spec.name)
49
+ # Binary is valid for other hosts; don't delete from backend.
50
+ return super
51
+ end
52
+
41
53
  installed = begin
42
54
  install_from_cache(cached_gem)
43
55
  rescue StandardError => e
@@ -78,6 +90,10 @@ module Prebake
78
90
  end
79
91
  end
80
92
 
93
+ def install_without_native_extension
94
+ mark_build_complete
95
+ end
96
+
81
97
  def install_from_cache(gem_path)
82
98
  count = Extractor.install(gem_path, @spec)
83
99
 
@@ -86,9 +102,13 @@ module Prebake
86
102
  return false
87
103
  end
88
104
 
105
+ mark_build_complete
106
+ true
107
+ end
108
+
109
+ def mark_build_complete
89
110
  FileUtils.mkdir_p(File.dirname(@spec.gem_build_complete_path))
90
111
  FileUtils.touch(@spec.gem_build_complete_path)
91
- true
92
112
  end
93
113
  end
94
114
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Prebake
6
+ module Glibc
7
+ GNU_LIBC_PATTERN = /GLIBC\s+(\d+(?:\.\d+)+)|GNU libc[^\n]*\s(\d+(?:\.\d+)+)/
8
+
9
+ def self.compatible?(required)
10
+ return true unless linux?
11
+ return true if required.nil?
12
+
13
+ detected = detected_version
14
+ return false if detected.nil?
15
+
16
+ Gem::Version.new(detected) >= Gem::Version.new(required)
17
+ end
18
+
19
+ def self.detected_version
20
+ return @detected_version if defined?(@detected_version)
21
+
22
+ output = run_ldd
23
+ @detected_version = output ? parse_version(output) : nil
24
+ end
25
+
26
+ def self.parse_version(output)
27
+ match = output.match(GNU_LIBC_PATTERN)
28
+ match && (match[1] || match[2])
29
+ end
30
+
31
+ def self.linux?
32
+ RUBY_PLATFORM.include?("linux")
33
+ end
34
+
35
+ def self.reset!
36
+ remove_instance_variable(:@detected_version) if defined?(@detected_version)
37
+ end
38
+
39
+ def self.run_ldd
40
+ out, status = Open3.capture2e("ldd", "--version")
41
+ return nil unless status.success?
42
+
43
+ out
44
+ rescue Errno::ENOENT
45
+ nil
46
+ end
47
+ end
48
+ end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Prebake
4
4
  module Logger
5
- LEVELS = { debug: 0, info: 1, warn: 2 }.freeze
5
+ LEVELS = { debug: 0, info: 1, warn: 2, silent: 3 }.freeze
6
6
 
7
7
  def self.level
8
- @level ||= LEVELS.fetch(ENV.fetch("PREBAKE_LOG_LEVEL", "warn").to_sym, 1)
8
+ @level ||= LEVELS.fetch(ENV.fetch("PREBAKE_LOG_LEVEL", "silent").to_sym, 3)
9
9
  end
10
10
 
11
11
  def self.debug(msg)
@@ -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
@@ -19,6 +20,18 @@ module Prebake
19
20
  enabled? && ENV.fetch("PREBAKE_PUSH_ENABLED", "false") == "true"
20
21
  end
21
22
 
23
+ def self.skip_portability_check?
24
+ ENV.fetch("PREBAKE_SKIP_PORTABILITY_CHECK", "false") == "true"
25
+ end
26
+
27
+ def self.max_glibc
28
+ ENV.fetch("PREBAKE_MAX_GLIBC", nil)
29
+ end
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
+
22
35
  def self.backend
23
36
  return @backend if defined?(@backend_loaded)
24
37
 
@@ -41,6 +54,7 @@ module Prebake
41
54
  def self.reset!
42
55
  remove_instance_variable(:@backend_loaded) if defined?(@backend_loaded)
43
56
  remove_instance_variable(:@backend) if defined?(@backend)
57
+ StaticRuby.reset!
44
58
  end
45
59
 
46
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.9
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-03-31 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
@@ -31,13 +31,17 @@ files:
31
31
  - lib/prebake/backends/http_client.rb
32
32
  - lib/prebake/backends/s3.rb
33
33
  - lib/prebake/cache_key.rb
34
+ - lib/prebake/elf_inspector.rb
34
35
  - lib/prebake/ext_builder_patch.rb
35
36
  - lib/prebake/extension_validator.rb
36
37
  - lib/prebake/extractor.rb
38
+ - lib/prebake/glibc.rb
37
39
  - lib/prebake/hooks.rb
38
40
  - lib/prebake/logger.rb
39
41
  - lib/prebake/platform.rb
40
42
  - lib/prebake/platform_gem_builder.rb
43
+ - lib/prebake/portability_guard.rb
44
+ - lib/prebake/static_ruby.rb
41
45
  - plugins.rb
42
46
  homepage: https://github.com/gembakery/prebake
43
47
  licenses: