bundler-source-pathext 0.3.0 → 0.4.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: c60ff2869c3dd24a8af2baf53c12732d91ab94f7e0da7118ee89d69564c2fd60
4
- data.tar.gz: dbb33d23f6d3958b156c6d3549f5faaecabd2955ed2a056258af62b48577c500
3
+ metadata.gz: a9746323d874d99471d27cbbec67b46d871fc5076327908f4e6165ad77f61961
4
+ data.tar.gz: 7fb084483775bd60058988c143ec08278f334d1db030db17870c0ec0a3cc87b7
5
5
  SHA512:
6
- metadata.gz: a39f6d46858cd5a27108e937cb30a57238d3d73170a4151ec0511b9c5e2a63146921d65a1f4d27abb9d24cf65608456a97cfca8958f1f4cc558301747431d707
7
- data.tar.gz: '0883191629d3506483e81fbeb2ed687ba666a79eb5c5034e16d5d0c1deea2bd14a49d013938359584304a498a4fdb4fb4c98bf07fd24660d74f57144ca02ef00'
6
+ metadata.gz: c0d89393e1cf197a70eff29629f64923556ed49e5ceb3e21433b1f55150ceae3c3270489cfb490365381778d4e965dc7d6dfd6d5c2ef4b3e17fc6e3417f68ec0
7
+ data.tar.gz: 185fe3d552b52908b7bacab0754be883c1599efccfb9e8f73f067f9fc6f9545615ea52e157fb87731cb3ae0e850af78dd5171f128b31685c974fb40513aef65c
@@ -0,0 +1,34 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Ruby ${{ matrix.ruby }} / Bundler ${{ matrix.bundler }} / ${{ matrix.os }}
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest]
16
+ ruby: ['3.2', '3.3', '3.4', '4.0']
17
+ bundler: ['2.5.22', '2.6.9', 'latest']
18
+ include:
19
+ - os: macos-latest
20
+ ruby: '4.0'
21
+ bundler: 'latest'
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ bundler: ${{ matrix.bundler }}
29
+
30
+ - run: bundle --version
31
+
32
+ # Note this intentionally does not run through bundler, since the tests
33
+ # shell out to "bundle install" in a temporary app of their own
34
+ - run: ruby test/integration_test.rb
data/README.md CHANGED
@@ -1,8 +1,13 @@
1
- # bundle-source-pathext [ ![](https://img.shields.io/gem/v/bundle-source-pathext.svg)](https://rubygems.org/gems/bundle-source-pathext) [ ![](https://img.shields.io/gem/dt/bundle-source-pathext.svg)](https://rubygems.org/gems/bundle-source-pathext)
1
+ # bundler-source-pathext
2
+ [ ![](https://img.shields.io/gem/v/bundler-source-pathext.svg)](https://rubygems.org/gems/bundler-source-pathext)
3
+ [ ![](https://img.shields.io/gem/dt/bundler-source-pathext.svg)](https://rubygems.org/gems/bundler-source-pathext)
4
+ [ ![](https://github.com/pganalyze/bundler-path-build-ext/actions/workflows/test.yml/badge.svg)](https://github.com/pganalyze/bundler-path-build-ext/actions/workflows/test.yml)
2
5
 
3
- This bundler plugin allows building local Ruby extensions that are referred to by a local path, similar to how gems are built when they are fetched from a remote path.
6
+ This bundler plugin allows building local Ruby extensions that are referred to by a local path,
7
+ similar to how gems are built when they are fetched from a remote path.
4
8
 
5
- Rubygems/Bundler itself unfortunately does not build local extensions automatically, making workflows complicated that utilize gems with an extension build, as part of an application.
9
+ Rubygems/Bundler itself unfortunately does not build local extensions automatically, making
10
+ workflows complicated that utilize gems with an extension build, as part of an application.
6
11
 
7
12
  ## Usage
8
13
 
@@ -14,7 +19,22 @@ source './folder', type: 'pathext' do
14
19
  end
15
20
  ```
16
21
 
17
- Different from Rubygems, this plugin uses a modified version of [Gem::Ext::Builder](https://github.com/ruby/rubygems/blob/master/lib/rubygems/ext/builder.rb) for Ruby extensions with an "extconf" folder, that does not create an additional copy of the created binaries in the extension install folder. This is done to avoid caching issues where the version in the extension install folder gets stale.
22
+ Different from Rubygems, this plugin uses a modified version of
23
+ [Gem::Ext::Builder](https://github.com/ruby/rubygems/blob/master/lib/rubygems/ext/builder.rb) for
24
+ Ruby extensions with an "extconf" folder.
25
+
26
+ Rubygems installs the built binaries into the gem's `lib` folder, and keeps a second copy in the
27
+ extension install folder, but only creates that copy when it does not exist yet. For a local gem,
28
+ whose version does not change when its source does, that second copy goes stale. This plugin
29
+ installs into a single location instead, and always overwrites it: the gem's `lib` folder when
30
+ Rubygems installs extensions there (which is the default, see `install_extension_in_lib`), and the
31
+ extension install folder (`tmp/PLATFORM/GEM/RUBY_VERSION` inside the gem) otherwise. Note that
32
+ cross-compilation is not supported.
33
+
34
+ ## Compatibility
35
+
36
+ Tested against Bundler 2.5, 2.6 and the current release, see the
37
+ [test workflow](.github/workflows/test.yml) for the versions that are covered.
18
38
 
19
39
  ## LICENSE
20
40
 
data/Rakefile CHANGED
@@ -1,2 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
- task :default => :spec
2
+
3
+ desc "Run the integration tests"
4
+ task :test do
5
+ ruby "test/integration_test.rb"
6
+ end
7
+
8
+ task :default => :test
@@ -1,12 +1,15 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "bundler-source-pathext"
3
- spec.version = "0.3.0"
3
+ spec.version = "0.4.0"
4
4
  spec.authors = ["pganalyze Team"]
5
5
  spec.email = ["team@pganalyze.com"]
6
6
 
7
- spec.summary = ''
8
- spec.description = ''
9
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
7
+ spec.summary = 'Bundler source plugin for local gems with native extensions'
8
+ spec.description = 'Bundler source plugin that works like a path source, but also builds the ' \
9
+ 'native extensions of the gems it provides, like a remote gem source does.'
10
+ spec.homepage = 'https://github.com/pganalyze/bundler-path-build-ext'
11
+ spec.license = 'BSD-3-Clause'
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
10
13
 
11
14
  # Specify which files should be added to the gem when it is released.
12
15
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rubygems/ext'
5
+
6
+ class BundlerSourcePathext < Bundler::Plugin::API
7
+ # Gem::Ext::Builder gained a target_rbconfig argument (for cross-compilation)
8
+ # in Rubygems 3.6, and an n_jobs keyword argument in Rubygems 4.0.2
9
+ HAS_TARGET_RBCONFIG = Gem.rubygems_version >= Gem::Version.new("3.6")
10
+ HAS_NJOBS = Gem.rubygems_version >= Gem::Version.new("4.0.2")
11
+
12
+ class PathExtSource < Bundler::Source
13
+ # Called by Bundler once per gem that gets installed from this source.
14
+ #
15
+ # Note that `opts` are the install options passed in by Bundler (:build_args,
16
+ # :previous_spec, ...), whereas `options` are the source options that came
17
+ # from the Gemfile or the lockfile.
18
+ def install(spec, opts = {})
19
+ using_message = "Using #{version_message(spec, opts[:previous_spec])} from #{self}"
20
+ using_message += " and installing its executables" unless spec.executables.empty?
21
+ print_using_message using_message
22
+
23
+ # Bundler's installer would build the extensions into the regular extension
24
+ # directory, we build them ourselves below instead
25
+ generate_bin(spec, disable_extensions: true)
26
+
27
+ build_local_extensions(spec, opts[:build_args])
28
+
29
+ nil # no post-install message
30
+ end
31
+
32
+ # Bundler plugin api, we need to return a Bundler::Index
33
+ def specs
34
+ # A relative path in the Gemfile is relative to the Gemfile itself (i.e.
35
+ # the bundler root), and not to the directory bundler was run from
36
+ files = Dir.glob(File.join(File.expand_path(uri, root), '*.gemspec'))
37
+
38
+ Bundler::Index.build do |index|
39
+ files.each do |file|
40
+ next unless spec = Bundler.load_gemspec(file)
41
+ spec.installed_by_version = Gem::VERSION
42
+ spec.source = self
43
+ spec.extension_dir = File.join(File.dirname(file), 'tmp', RUBY_PLATFORM, spec.name, RUBY_VERSION)
44
+ Bundler.rubygems.validate(spec)
45
+
46
+ index << spec
47
+ end
48
+ end
49
+ end
50
+
51
+ # The gems are always used in place, so there is nothing to fetch, cache or
52
+ # refresh here - all of these are intentional no-ops:
53
+ #
54
+ # local! - fetch the specs and install from the local system
55
+ # remote! - fetch the specs and install from a remote path
56
+ # cached! - fetch the specs and install from `app_cache_path`
57
+ # unlock! - refresh the cache/specs (e.g. git sources make a fresh clone)
58
+ def local!; end
59
+ def remote!; end
60
+ def cached!; end
61
+ def unlock!; end
62
+
63
+ private
64
+
65
+ def build_local_extensions(spec, build_args = nil)
66
+ # Bundler passes in whatever "bundle config build.GEM" is set to, and the
67
+ # source options allow setting build args for all gems of a source
68
+ build_args = Array(build_args)
69
+ build_args = Array(options['build_args']) if build_args.empty?
70
+ build_args = Array(Bundler.rubygems.build_args) if build_args.empty?
71
+
72
+ builder = Gem::Ext::Builder.new spec, build_args
73
+
74
+ build_extensions(spec, build_args, builder)
75
+ end
76
+
77
+ def build_extensions(spec, build_args, builder)
78
+ return if spec.extensions.empty?
79
+
80
+ if build_args.empty?
81
+ Bundler.ui.info "Building native extensions for #{spec.name}. This could take a while..."
82
+ else
83
+ Bundler.ui.info "Building native extensions for #{spec.name} with: '#{build_args.join " "}'"
84
+ Bundler.ui.info "This could take a while..."
85
+ end
86
+
87
+ dest_path = spec.extension_dir
88
+ start = Time.now
89
+
90
+ spec.extensions.each do |extension|
91
+ builder_for_ext = if extension[/extconf/]
92
+ ExtConfAlwaysCopyBuilder
93
+ else
94
+ builder.builder_for(extension)
95
+ end
96
+
97
+ # This throws a Gem::Ext::BuildError if building the extension fails
98
+ build_extension spec, builder, builder_for_ext, extension, dest_path, build_args
99
+ end
100
+ Bundler.ui.info format(' Finished %s after %0.2f seconds', spec.name, Time.now - start)
101
+ FileUtils.touch(spec.gem_build_complete_path)
102
+ end
103
+
104
+ def build_extension(spec, builder, builder_for_ext, extension, dest_path, build_args) # :nodoc:
105
+ results = []
106
+
107
+ extension_dir =
108
+ File.expand_path File.join(spec.full_gem_path, File.dirname(extension))
109
+ lib_dir = File.join spec.full_gem_path, spec.raw_require_paths.first
110
+
111
+ begin
112
+ FileUtils.mkdir_p dest_path
113
+
114
+ results = builder_for_ext.build(extension, dest_path,
115
+ results, build_args, lib_dir, extension_dir)
116
+
117
+ builder.verbose { results.join("\n") }
118
+
119
+ builder.write_gem_make_out results.join "\n"
120
+ rescue StandardError => e
121
+ results << e.message
122
+ builder.build_error(results.join("\n"), $@)
123
+ end
124
+ end
125
+
126
+ def generate_bin(spec, options = {})
127
+ gem_dir = Pathname.new(spec.full_gem_path)
128
+
129
+ # Some gem authors put absolute paths in their gemspec
130
+ # and we have to save them from themselves
131
+ spec.files = spec.files.filter_map do |path|
132
+ pathname = Pathname.new(path)
133
+ next path unless pathname.absolute?
134
+ next if File.directory?(path)
135
+ begin
136
+ pathname.relative_path_from(gem_dir).to_s
137
+ rescue ArgumentError
138
+ path
139
+ end
140
+ end
141
+
142
+ installer = Bundler::Source::Path::Installer.new(
143
+ spec,
144
+ env_shebang: false,
145
+ disable_extensions: options[:disable_extensions],
146
+ build_args: options[:build_args],
147
+ bundler_extension_cache_path: extension_cache_path(spec)
148
+ )
149
+ installer.post_install
150
+ rescue Gem::InvalidSpecificationException => e
151
+ Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
152
+ "This prevents bundler from installing bins or native extensions, but " \
153
+ "that may not affect its functionality."
154
+
155
+ if !spec.extensions.empty? && !spec.email.empty?
156
+ Bundler.ui.warn "If you need to use this package without installing it from a gem " \
157
+ "repository, please contact #{spec.email} and ask them " \
158
+ "to modify their .gemspec so it can work with `gem build`."
159
+ end
160
+
161
+ Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}"
162
+ end
163
+ end
164
+
165
+ # Modified version of Gem::Ext::ExtConfBuilder that always copies the built binary
166
+ # to the destination path. This is necessary because when using local gems we cannot
167
+ # rely on versions being bumped when the gem changes.
168
+ class ExtConfAlwaysCopyBuilder < Gem::Ext::Builder
169
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
170
+ target_rbconfig = nil, n_jobs: nil)
171
+ require "fileutils"
172
+ require "tempfile"
173
+
174
+ target_rbconfig ||= Gem.target_rbconfig if HAS_TARGET_RBCONFIG
175
+
176
+ tmp_dest = Dir.mktmpdir(".gem.", extension_dir)
177
+
178
+ # Some versions of `mktmpdir` return absolute paths, which will break make
179
+ # if the paths contain spaces.
180
+ #
181
+ # As such, we convert to a relative path.
182
+ tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir)
183
+
184
+ destdir = ENV["DESTDIR"]
185
+
186
+ begin
187
+ cmd = ruby << File.basename(extension)
188
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if HAS_TARGET_RBCONFIG && target_rbconfig.path
189
+ cmd.push(*args)
190
+
191
+ run(cmd, results, 'ExtConfAlwaysCopy', extension_dir) do |s, r|
192
+ mkmf_log = File.join(extension_dir, "mkmf.log")
193
+ if File.exist? mkmf_log
194
+ unless s.success?
195
+ r << "To see why this extension failed to compile, please check" \
196
+ " the mkmf.log which can be found here:\n"
197
+ r << " " + File.join(dest_path, "mkmf.log") + "\n"
198
+ end
199
+ FileUtils.mv mkmf_log, dest_path
200
+ end
201
+ end
202
+
203
+ ENV["DESTDIR"] = nil
204
+
205
+ if HAS_TARGET_RBCONFIG && HAS_NJOBS
206
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
207
+ elsif HAS_TARGET_RBCONFIG
208
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig
209
+ else
210
+ make dest_path, results, extension_dir, tmp_dest_relative
211
+ end
212
+
213
+ full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
214
+
215
+ is_cross_compiling = HAS_TARGET_RBCONFIG && target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
216
+
217
+ entries = (Dir.entries(full_tmp_dest) - %w[. ..]).map {|entry| File.join full_tmp_dest, entry }
218
+
219
+ # MODIFIED
220
+ # Rubygems copies the built files into the gem's lib directory, and then
221
+ # additionally moves them into the extension install directory, but only
222
+ # the ones that don't exist there yet. We install into a single location,
223
+ # always overwriting, so a rebuilt extension leaves no stale binary behind.
224
+ #
225
+ # Note that install_extension_in_lib can be turned off through gemrc, and
226
+ # that Rubygems intends to eventually default it to false, which would
227
+ # make the extension directory the regular case rather than a fallback.
228
+ #
229
+ # Rubygems skips the copy into lib when cross-compiling, because that
230
+ # directory is shared with the build for the host platform, and relies on
231
+ # the extension install directory being specific to the target (it follows
232
+ # Gem::Platform.local, which is derived from the target rbconfig). The
233
+ # directory we use is keyed to the platform of the running Ruby, so we
234
+ # have nowhere to put a cross-compiled build, and skip it entirely.
235
+ unless is_cross_compiling
236
+ if Gem.install_extension_in_lib && lib_dir
237
+ FileUtils.mkdir_p lib_dir
238
+ FileUtils.cp_r entries, lib_dir, remove_destination: true
239
+ else
240
+ FileUtils.cp_r entries, dest_path, remove_destination: true
241
+ end
242
+ end
243
+
244
+ if HAS_TARGET_RBCONFIG
245
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
246
+ else
247
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"]
248
+ end
249
+ ensure
250
+ ENV["DESTDIR"] = destdir
251
+ end
252
+
253
+ results
254
+ rescue => error
255
+ if defined?(Gem::Ext::Builder::NoMakefileError) && error.is_a?(Gem::Ext::Builder::NoMakefileError)
256
+ results << error.message
257
+ results << "Skipping make for #{extension} as no Makefile was found."
258
+ # We are good, do not re-raise the error.
259
+ else
260
+ raise
261
+ end
262
+ ensure
263
+ FileUtils.rm_rf tmp_dest if tmp_dest
264
+ end
265
+
266
+ def self.get_relative_path(path, base)
267
+ path[0..base.length - 1] = "." if path.start_with?(base)
268
+ path
269
+ end
270
+ end
271
+ end
data/plugins.rb CHANGED
@@ -1,266 +1,9 @@
1
- class BundlerSourcePathext < Bundler::Plugin::API
2
- HAS_TARGET_RBCONFIG = Gem.rubygems_version >= Gem::Version.new("3.6")
3
- HAS_NJOBS = Gem.rubygems_version >= Gem::Version.new("4.0.2")
1
+ # frozen_string_literal: true
4
2
 
5
- class PathExtSource < Bundler::Source
6
- def install(spec, opts)
7
- print_using_message "Using #{spec.name} #{spec.version} from #{self}"
3
+ require 'bundler-source-pathext'
8
4
 
9
- using_message = "Using #{version_message(spec, options[:previous_spec])} from #{self}"
10
- using_message += " and installing its executables" unless spec.executables.empty?
11
- print_using_message using_message
12
- generate_bin(spec, disable_extensions: true) # Turned off!
13
-
14
- build_local_extensions(spec)
15
-
16
- nil # no post-install message
17
- end
18
-
19
- # Bundler plugin api, we need to return a Bundler::Index
20
- def specs
21
- files = Dir.glob(File.join(File.expand_path(uri), '*.gemspec'))
22
-
23
- Bundler::Index.build do |index|
24
- files.each do |file|
25
- next unless spec = Bundler.load_gemspec(file)
26
- spec.installed_by_version = Gem::VERSION
27
- spec.source = self
28
- spec.extension_dir = File.join(File.dirname(file), 'tmp', RUBY_PLATFORM, spec.name, RUBY_VERSION)
29
- Bundler.rubygems.validate(spec)
30
-
31
- index << spec
32
- end
33
- end
34
- end
35
-
36
- # Set internal representation to fetch the gems/specs locally.
37
- #
38
- # When this is called, the source should try to fetch the specs and
39
- # install from the local system.
40
- def local!
41
- # not applicable
42
- end
43
-
44
- # Set internal representation to fetch the gems/specs from remote.
45
- #
46
- # When this is called, the source should try to fetch the specs and
47
- # install from remote path.
48
- def remote!
49
- # not applicable
50
- end
51
-
52
- # Set internal representation to fetch the gems/specs from app cache.
53
- #
54
- # When this is called, the source should try to fetch the specs and
55
- # install from the path provided by `app_cache_path`.
56
- def cached!
57
- # not applicable
58
- end
59
-
60
- # This is called to update the spec and installation.
61
- #
62
- # If the source plugin is loaded from lockfile or otherwise, it shall
63
- # refresh the cache/specs (e.g. git sources can make a fresh clone).
64
- def unlock!
65
- # not applicable
66
- end
67
-
68
- private
69
-
70
- def build_local_extensions(spec)
71
- build_args = options[:build_args] || Bundler.rubygems.build_args || begin
72
- require_relative "command"
73
- Gem::Command.build_args
74
- end
75
-
76
- builder = Gem::Ext::Builder.new spec, build_args
77
-
78
- build_extensions(spec, build_args, builder)
79
- end
80
-
81
- def build_extensions(spec, build_args, builder)
82
- return if spec.extensions.empty?
83
-
84
- if build_args.empty?
85
- puts "Building native extensions for #{spec.name}. This could take a while..."
86
- else
87
- puts "Building native extensions for #{spec.name} with: '#{@build_args.join " "}'"
88
- puts "This could take a while..."
89
- end
90
-
91
- dest_path = spec.extension_dir
92
- start = Time.now
93
-
94
- spec.extensions.each do |extension|
95
- builder_for_ext = if extension[/extconf/]
96
- ExtConfAlwaysCopyBuilder
97
- else
98
- builder.builder_for(extension)
99
- end
100
-
101
- # This throws a Gem::Ext::BuildError if building the extension fails
102
- build_extension spec, builder, builder_for_ext, extension, dest_path
103
- end
104
- puts format(' Finished %s after %0.2f seconds', spec.name, Time.now - start)
105
- FileUtils.touch(spec.gem_build_complete_path)
106
- end
107
-
108
- def build_extension(spec, builder, builder_for_ext, extension, dest_path) # :nodoc:
109
- results = []
110
-
111
- extension_dir =
112
- File.expand_path File.join(spec.full_gem_path, File.dirname(extension))
113
- lib_dir = File.join spec.full_gem_path, spec.raw_require_paths.first
114
-
115
- begin
116
- FileUtils.mkdir_p dest_path
117
-
118
- results = builder_for_ext.build(extension, dest_path,
119
- results, @build_args, lib_dir, extension_dir)
120
-
121
- builder.verbose { results.join("\n") }
122
-
123
- builder.write_gem_make_out results.join "\n"
124
- rescue StandardError => e
125
- results << e.message
126
- builder.build_error(results.join("\n"), $@)
127
- end
128
- end
129
-
130
- def generate_bin(spec, options = {})
131
- gem_dir = Pathname.new(spec.full_gem_path)
132
-
133
- # Some gem authors put absolute paths in their gemspec
134
- # and we have to save them from themselves
135
- spec.files = spec.files.filter_map do |path|
136
- next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path)
137
- next if File.directory?(path)
138
- begin
139
- Pathname.new(path).relative_path_from(gem_dir).to_s
140
- rescue ArgumentError
141
- path
142
- end
143
- end
144
-
145
- installer = Path::Installer.new(
146
- spec,
147
- env_shebang: false,
148
- disable_extensions: options[:disable_extensions],
149
- build_args: options[:build_args],
150
- bundler_extension_cache_path: extension_cache_path(spec)
151
- )
152
- installer.post_install
153
- rescue Gem::InvalidSpecificationException => e
154
- Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
155
- "This prevents bundler from installing bins or native extensions, but " \
156
- "that may not affect its functionality."
157
-
158
- if !spec.extensions.empty? && !spec.email.empty?
159
- Bundler.ui.warn "If you need to use this package without installing it from a gem " \
160
- "repository, please contact #{spec.email} and ask them " \
161
- "to modify their .gemspec so it can work with `gem build`."
162
- end
163
-
164
- Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}"
165
- end
166
- end
167
-
168
- source 'pathext', PathExtSource
169
-
170
- # Modified version of Gem::Ext::ExtConfBuilder that always copies the built binary
171
- # to the destination path. This is necessary because when using local gems we cannot
172
- # rely on versions being bumped when the gem changes.
173
- require 'rubygems/ext'
174
- class ExtConfAlwaysCopyBuilder < Gem::Ext::Builder
175
- def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
176
- target_rbconfig = nil, n_jobs: nil)
177
- require "fileutils"
178
- require "tempfile"
179
-
180
- target_rbconfig ||= Gem.target_rbconfig if HAS_TARGET_RBCONFIG
181
-
182
- tmp_dest = Dir.mktmpdir(".gem.", extension_dir)
183
-
184
- # Some versions of `mktmpdir` return absolute paths, which will break make
185
- # if the paths contain spaces.
186
- #
187
- # As such, we convert to a relative path.
188
- tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir)
189
-
190
- destdir = ENV["DESTDIR"]
191
-
192
- begin
193
- cmd = ruby << File.basename(extension)
194
- cmd << "--target-rbconfig=#{target_rbconfig.path}" if HAS_TARGET_RBCONFIG && target_rbconfig.path
195
- cmd.push(*args)
196
-
197
- run(cmd, results, 'ExtConfAlwaysCopy', extension_dir) do |s, r|
198
- mkmf_log = File.join(extension_dir, "mkmf.log")
199
- if File.exist? mkmf_log
200
- unless s.success?
201
- r << "To see why this extension failed to compile, please check" \
202
- " the mkmf.log which can be found here:\n"
203
- r << " " + File.join(dest_path, "mkmf.log") + "\n"
204
- end
205
- FileUtils.mv mkmf_log, dest_path
206
- end
207
- end
208
-
209
- ENV["DESTDIR"] = nil
210
-
211
- if HAS_TARGET_RBCONFIG && HAS_NJOBS
212
- make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
213
- elsif HAS_TARGET_RBCONFIG
214
- make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig
215
- else
216
- make dest_path, results, extension_dir, tmp_dest_relative
217
- end
218
-
219
- full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
220
-
221
- is_cross_compiling = HAS_TARGET_RBCONFIG && target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
222
- # Do not copy extension libraries by default when cross-compiling
223
- # not to conflict with the one already built for the host platform.
224
- if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling
225
- FileUtils.mkdir_p lib_dir
226
- entries = Dir.entries(full_tmp_dest) - %w[. ..]
227
- entries = entries.map {|entry| File.join full_tmp_dest, entry }
228
- FileUtils.cp_r entries, lib_dir, remove_destination: true
229
- end
230
-
231
- # MODIFIED
232
- # We skip installing into the destination directory, because we can rely on the copy in lib/ instead
233
- # This also causes caching issues with stale files
234
- #FileUtils::Entry_.new(full_tmp_dest).traverse do |ent|
235
- # destent = ent.class.new(dest_path, ent.rel)
236
- # destent.exist? || FileUtils.mv(ent.path, destent.path)
237
- #end
238
-
239
- if HAS_TARGET_RBCONFIG
240
- make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
241
- else
242
- make dest_path, results, extension_dir, tmp_dest_relative, ["clean"]
243
- end
244
- ensure
245
- ENV["DESTDIR"] = destdir
246
- end
247
-
248
- results
249
- rescue => error
250
- if defined?(Gem::Ext::Builder::NoMakefileError) && error.is_a?(Gem::Ext::Builder::NoMakefileError)
251
- results << error.message
252
- results << "Skipping make for #{extension} as no Makefile was found."
253
- # We are good, do not re-raise the error.
254
- else
255
- raise
256
- end
257
- ensure
258
- FileUtils.rm_rf tmp_dest if tmp_dest
259
- end
260
-
261
- def self.get_relative_path(path, base)
262
- path[0..base.length - 1] = "." if path.start_with?(base)
263
- path
264
- end
265
- end
266
- end
5
+ # Note that Bundler loads this file once when the plugin is installed, and again
6
+ # when the plugin is used, and only keeps the sources registered by the latter.
7
+ # The registration therefore has to happen here, and not in the required file,
8
+ # which only gets loaded once per process.
9
+ BundlerSourcePathext.source 'pathext', BundlerSourcePathext::PathExtSource
metadata CHANGED
@@ -1,22 +1,24 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-source-pathext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pganalyze Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-17 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: ''
13
+ description: Bundler source plugin that works like a path source, but also builds
14
+ the native extensions of the gems it provides, like a remote gem source does.
14
15
  email:
15
16
  - team@pganalyze.com
16
17
  executables: []
17
18
  extensions: []
18
19
  extra_rdoc_files: []
19
20
  files:
21
+ - ".github/workflows/test.yml"
20
22
  - ".gitignore"
21
23
  - Gemfile
22
24
  - Gemfile.lock
@@ -24,9 +26,11 @@ files:
24
26
  - README.md
25
27
  - Rakefile
26
28
  - bundler-source-pathext.gemspec
29
+ - lib/bundler-source-pathext.rb
27
30
  - plugins.rb
28
- homepage:
29
- licenses: []
31
+ homepage: https://github.com/pganalyze/bundler-path-build-ext
32
+ licenses:
33
+ - BSD-3-Clause
30
34
  metadata: {}
31
35
  post_install_message:
32
36
  rdoc_options: []
@@ -36,7 +40,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
40
  requirements:
37
41
  - - ">="
38
42
  - !ruby/object:Gem::Version
39
- version: 2.3.0
43
+ version: 2.7.0
40
44
  required_rubygems_version: !ruby/object:Gem::Requirement
41
45
  requirements:
42
46
  - - ">="
@@ -46,5 +50,5 @@ requirements: []
46
50
  rubygems_version: 3.5.22
47
51
  signing_key:
48
52
  specification_version: 4
49
- summary: ''
53
+ summary: Bundler source plugin for local gems with native extensions
50
54
  test_files: []