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 +4 -4
- data/lib/prebake/async_publisher.rb +16 -0
- data/lib/prebake/elf_inspector.rb +79 -0
- data/lib/prebake/ext_builder_patch.rb +21 -1
- data/lib/prebake/glibc.rb +48 -0
- data/lib/prebake/logger.rb +2 -2
- data/lib/prebake/portability_guard.rb +38 -0
- data/lib/prebake/static_ruby.rb +40 -0
- data/lib/prebake.rb +14 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27e9af4988afacc2c07ae6e09eac8aa9efa26d44dc95401cd4f1d52e14993bd6
|
|
4
|
+
data.tar.gz: 801beefc4a2eed88cda591e6cf0b6abbef13a8f597f185eececeafd49b9e6857
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/prebake/logger.rb
CHANGED
|
@@ -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", "
|
|
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.
|
|
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-
|
|
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:
|