prebake 0.3.2 → 0.3.3

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: 165d7d78f5647f35f50690133586dfd1c7d18b55958a184f20f812cd322587ce
4
- data.tar.gz: e0f7ee51e509b3961bfaa812d142e4d84a1ea1ba8a110fa8330b5da74165e638
3
+ metadata.gz: f8631c361d4db2193eacf4995fb4127aff87dac604181479f2a4d8dcce984da7
4
+ data.tar.gz: eec69b8190a68c262659ab98e134174a5ea06f9b1ffa25c204bc3b03de5dcb1a
5
5
  SHA512:
6
- metadata.gz: eeeb9c110e82c848a208738c35e565c9590586f7848e782524de04e6482fd9fa378da70a9be3191a2e40821c09ddcd7273d2ec2963028d7bfab6783addd90c4c
7
- data.tar.gz: 22d403d28812c4ff75a3f620ff67d8761ffcfb2917727233e55d6d7b59687e689256ec42ec81685ccbbdeb05d27756e737f76b3c5a10bb4ec2f9ee0eca82c06e
6
+ metadata.gz: 58878c01e36b7553feba5fc0077b2affb94d1188aa53e8e65839fd047d89d8e9089c5eae6a1c7258fccf13852e7cc1ea86131711948e301511f8db64294a712f
7
+ data.tar.gz: 3860c5f16d6765e4fbff18c1049e3402610c9961d07cf8cec61115352be954ddf647e37c9c0a6b7055e62bdb6ea4cc3ce7c041ef6306302a8daac29dc91e5643
@@ -76,7 +76,8 @@ module Prebake
76
76
  end
77
77
 
78
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"
79
+ Logger.warn "Skipping push of #{cache_key}: binary requires libruby.so (dynamic Ruby) " \
80
+ "but this is a static Ruby build; binary would crash on this platform"
80
81
  FileUtils.rm_f(gem_path)
81
82
  return nil
82
83
  end
@@ -10,7 +10,10 @@ module Prebake
10
10
  module ElfInspector
11
11
  def self.required_glibc_for_gem(gem_path)
12
12
  versions = []
13
- each_gem_binary(gem_path) { |binary| v = required_glibc(binary); versions << v if v }
13
+ each_gem_binary(gem_path) do |binary|
14
+ v = required_glibc(binary)
15
+ versions << v if v
16
+ end
14
17
  versions.empty? ? nil : versions.max_by { |v| Gem::Version.new(v) }
15
18
  rescue StandardError => e
16
19
  # Malformed gem, missing objdump, or I/O error — treat as unknown, let
@@ -69,7 +72,7 @@ module Prebake
69
72
  Dir.mktmpdir("prebake-gem") do |tmpdir|
70
73
  Gem::Package.new(gem_path).extract_files(tmpdir)
71
74
  Dir.glob(File.join(tmpdir, "**/*.{so,bundle,dll}")).each do |binary|
72
- next if File.symlink?(binary) || File.size(binary).zero?
75
+ next if File.symlink?(binary) || File.empty?(binary)
73
76
 
74
77
  yield binary
75
78
  end
@@ -11,6 +11,14 @@ require_relative "logger"
11
11
 
12
12
  module Prebake
13
13
  module ExtBuilderPatch
14
+ # The metrics cops are off for this method deliberately. It is a cascade of
15
+ # cache checks where every failure path falls through to `super`, the real
16
+ # source build. Extracting any stage would mean returning a sentinel for the
17
+ # caller to turn back into `super`, which reads worse than the cascade and
18
+ # risks silently installing a gem with no compiled binaries if a branch is
19
+ # mistranslated.
20
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
21
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
14
22
  def build_extensions
15
23
  return super unless @spec.extensions.any?
16
24
  return super unless Prebake.enabled?
@@ -59,7 +67,7 @@ module Prebake
59
67
 
60
68
  unless installed
61
69
  Prebake.backend.delete(cache_key)
62
- return super
70
+ super
63
71
  end
64
72
  else
65
73
  super
@@ -67,6 +75,8 @@ module Prebake
67
75
  ensure
68
76
  FileUtils.rm_f(cached_gem) if cached_gem
69
77
  end
78
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
79
+ # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
70
80
 
71
81
  private
72
82
 
@@ -24,7 +24,7 @@ module Prebake
24
24
 
25
25
  nested.each do |binary|
26
26
  next if File.symlink?(binary)
27
- next if File.size(binary).zero?
27
+ next if File.empty?(binary)
28
28
 
29
29
  dest = File.join(ext_dir, File.basename(binary))
30
30
  next if File.exist?(dest)
@@ -17,35 +17,18 @@ module Prebake
17
17
 
18
18
  # Copy only binary files (.so, .bundle, .dll) to extension_dir
19
19
  Dir.glob(File.join(tmpdir, "**/*.{so,bundle,dll}")).each do |binary|
20
- # Reject symlinks and empty files
21
- next if File.symlink?(binary)
22
- next if File.size(binary).zero?
23
-
24
- # Verify path is within tmpdir (prevent traversal)
25
- real_binary = File.realpath(binary)
26
- real_tmpdir = File.realpath(tmpdir)
27
- next unless real_binary.start_with?("#{real_tmpdir}/")
28
-
29
- relative = binary.sub("#{tmpdir}/", "")
30
-
31
- # Normalize paths from cached gems where binaries were packaged
32
- # from gem_dir build artifacts or dirty extension_dirs.
33
- # ext/<name>/<name>.so → <name>.so (build artifact)
34
- # lib/<name>/<name>.so → <name>/<name>.so (gem lib path)
35
- # extension/<platform>/<ver>/<name>.so → <name>.so (extension_dir artifact)
36
- relative = relative.sub(%r{\Aext/[^/]+/}, "") if relative.start_with?("ext/")
37
- relative = relative.sub(%r{\Alib/}, "") if relative.start_with?("lib/")
38
- relative = relative.sub(%r{\Aextensions?/[^/]+/[^/]+/}, "") if relative.start_with?("extension/", "extensions/")
39
-
40
- dest = File.join(spec.extension_dir, relative)
20
+ next unless safe_binary?(binary, tmpdir)
21
+
22
+ dest = File.join(spec.extension_dir, normalized_path(binary, tmpdir))
41
23
  FileUtils.mkdir_p(File.dirname(dest))
42
24
  FileUtils.cp(binary, dest)
25
+ restore_in_gem_dir(binary, tmpdir, spec)
43
26
  extracted_count += 1
44
27
  end
45
28
  end
46
29
 
47
30
  # Mark this extension_dir as prebake-managed for post-install validation
48
- FileUtils.touch(File.join(spec.extension_dir, ".prebake")) if extracted_count > 0
31
+ FileUtils.touch(File.join(spec.extension_dir, ".prebake")) if extracted_count.positive?
49
32
 
50
33
  Logger.info "Installed precompiled #{File.basename(gem_path)} " \
51
34
  "(#{extracted_count} binary files)"
@@ -55,5 +38,42 @@ module Prebake
55
38
  Logger.warn "Extraction failed for #{File.basename(gem_path)}: #{e.message}"
56
39
  raise
57
40
  end
41
+
42
+ # Reject symlinks, empty files, and anything resolving outside tmpdir
43
+ # (path traversal via a crafted gem).
44
+ def self.safe_binary?(binary, tmpdir)
45
+ return false if File.symlink?(binary)
46
+ return false if File.empty?(binary)
47
+
48
+ File.realpath(binary).start_with?("#{File.realpath(tmpdir)}/")
49
+ end
50
+
51
+ # Normalize paths from cached gems where binaries were packaged from
52
+ # gem_dir build artifacts or dirty extension_dirs.
53
+ # ext/<name>/<name>.so → <name>.so (build artifact)
54
+ # lib/<name>/<name>.so → <name>/<name>.so (gem lib path)
55
+ # extension/<platform>/<ver>/<name>.so → <name>.so (extension_dir artifact)
56
+ def self.normalized_path(binary, tmpdir)
57
+ relative = binary.sub("#{tmpdir}/", "")
58
+ relative = relative.sub(%r{\Aext/[^/]+/}, "") if relative.start_with?("ext/")
59
+ relative = relative.sub(%r{\Alib/}, "") if relative.start_with?("lib/")
60
+ return relative unless relative.start_with?("extension/", "extensions/")
61
+
62
+ relative.sub(%r{\Aextensions?/[^/]+/[^/]+/}, "")
63
+ end
64
+
65
+ # A source build leaves binaries in the gem dir too: ffi-compiler builds in
66
+ # place under ext/, and RubyGems copies extconf output into lib/. FFI gems
67
+ # dlopen those exact paths (llhttp-ffi, sassc). Only ext/ and lib/ are
68
+ # restored, because self-hosted cached gems put extension_dir binaries at
69
+ # the gem root, where no build would leave them.
70
+ def self.restore_in_gem_dir(binary, tmpdir, spec)
71
+ relative = binary.delete_prefix("#{tmpdir}/")
72
+ return unless relative.start_with?("ext/", "lib/")
73
+
74
+ dest = File.join(spec.full_gem_path, relative)
75
+ FileUtils.mkdir_p(File.dirname(dest))
76
+ FileUtils.cp(binary, dest)
77
+ end
58
78
  end
59
79
  end
@@ -54,34 +54,7 @@ module Prebake
54
54
  Dir.glob(File.join(build_dir, "**/*.{so,bundle,dll}")).each { |f| File.delete(f) }
55
55
 
56
56
  ext_dir = @spec.extension_dir
57
- if ext_dir && File.directory?(ext_dir)
58
- # Collect binaries at root and one level deep (e.g., nokogiri/nokogiri.so).
59
- binaries = Dir.glob(File.join(ext_dir, "*.{so,bundle,dll}")) +
60
- Dir.glob(File.join(ext_dir, "*/*.{so,bundle,dll}"))
61
-
62
- # Ruby 4.0+ places compiled extensions in extension/<platform>/<abi>/
63
- # within extension_dir. Collect these too, normalizing their paths
64
- # to root level so the cached gem is layout-agnostic.
65
- Dir.glob(File.join(ext_dir, "extension/*/*/*.{so,bundle,dll}")).each do |binary|
66
- relative = binary.delete_prefix("#{ext_dir}/")
67
- normalized = relative.sub(%r{\Aextension/[^/]+/[^/]+/}, "")
68
- # Skip if a root-level binary with the same name already exists
69
- next if binaries.any? { |b| b.delete_prefix("#{ext_dir}/") == normalized }
70
-
71
- binaries << binary
72
- end
73
-
74
- binaries.each do |binary|
75
- next if File.symlink?(binary)
76
- next if File.size(binary).zero?
77
- relative = binary.delete_prefix("#{ext_dir}/")
78
- # Normalize extension/<platform>/<abi>/ paths to root level
79
- relative = relative.sub(%r{\Aextension/[^/]+/[^/]+/}, "")
80
- dest = File.join(build_dir, relative)
81
- FileUtils.mkdir_p(File.dirname(dest))
82
- FileUtils.cp(binary, dest)
83
- end
84
- end
57
+ copy_installed_binaries(ext_dir, build_dir) if ext_dir && File.directory?(ext_dir)
85
58
 
86
59
  prefix = "#{build_dir}/"
87
60
  compiled = Dir.glob(File.join(build_dir, "**/*.{so,bundle,dll}"))
@@ -90,5 +63,38 @@ module Prebake
90
63
 
91
64
  platform_spec
92
65
  end
66
+
67
+ def copy_installed_binaries(ext_dir, build_dir)
68
+ installed_binaries(ext_dir).each do |binary|
69
+ next if File.symlink?(binary)
70
+ next if File.empty?(binary)
71
+
72
+ dest = File.join(build_dir, root_level_path(binary, ext_dir))
73
+ FileUtils.mkdir_p(File.dirname(dest))
74
+ FileUtils.cp(binary, dest)
75
+ end
76
+ end
77
+
78
+ # Binaries at root and one level deep (e.g., nokogiri/nokogiri.so), plus the
79
+ # extension/<platform>/<abi>/ layout Ruby 4.0+ installs into. A nested
80
+ # binary is skipped when a root-level one already claims its normalized
81
+ # path, so the cached gem stays layout-agnostic.
82
+ def installed_binaries(ext_dir)
83
+ binaries = Dir.glob(File.join(ext_dir, "*.{so,bundle,dll}")) +
84
+ Dir.glob(File.join(ext_dir, "*/*.{so,bundle,dll}"))
85
+
86
+ Dir.glob(File.join(ext_dir, "extension/*/*/*.{so,bundle,dll}")).each do |binary|
87
+ normalized = root_level_path(binary, ext_dir)
88
+ next if binaries.any? { |b| b.delete_prefix("#{ext_dir}/") == normalized }
89
+
90
+ binaries << binary
91
+ end
92
+
93
+ binaries
94
+ end
95
+
96
+ def root_level_path(binary, ext_dir)
97
+ binary.delete_prefix("#{ext_dir}/").sub(%r{\Aextension/[^/]+/[^/]+/}, "")
98
+ end
93
99
  end
94
100
  end
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.3.2
4
+ version: 0.3.3
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-09-13 00:00:00.000000000 Z
11
+ date: 2026-09-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