bundler-source-pathext 0.3.0 → 0.4.1

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: 74f39d19bde0adf306918257f3c5fffb56084f083adbc66e56045f7662cfb445
4
+ data.tar.gz: 4369b733c6b795dc497f8aeaa6477bcd957b72583801a4279e63f0b2d8ca000e
5
5
  SHA512:
6
- metadata.gz: a39f6d46858cd5a27108e937cb30a57238d3d73170a4151ec0511b9c5e2a63146921d65a1f4d27abb9d24cf65608456a97cfca8958f1f4cc558301747431d707
7
- data.tar.gz: '0883191629d3506483e81fbeb2ed687ba666a79eb5c5034e16d5d0c1deea2bd14a49d013938359584304a498a4fdb4fb4c98bf07fd24660d74f57144ca02ef00'
6
+ metadata.gz: b51052c526b45864b3be82804cd2647eebdf462e73e1220232362d6cf86331b3aee1ecea49c892ae789769f8823027eccb951f8f0f159e3ce09086f648bd890d
7
+ data.tar.gz: d46411773b33d982fb0b03608670cdbca622cb5179e23a3675929989743a7eec943eebde776c16a8e40e5c3dd74d051d86adb196a5b8d7f77949dd607b10991e
@@ -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/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bundler-source-pathext (0.3.0)
4
+ bundler-source-pathext (0.4.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -11,10 +11,11 @@ GEM
11
11
  PLATFORMS
12
12
  arm64-darwin-24
13
13
  x86_64-darwin-17
14
+ x86_64-linux
14
15
 
15
16
  DEPENDENCIES
16
17
  bundler-source-pathext!
17
18
  rake (~> 12.0)
18
19
 
19
20
  BUNDLED WITH
20
- 2.2.3
21
+ 4.1.0.beta2
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.1"
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,314 @@
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
+ # Where each source's relative path is resolved from, keyed by its uri.
14
+ #
15
+ # Bundler builds a source twice: once from the Gemfile, and once from the
16
+ # lockfile, and it keeps the latter (see Definition#converge_sources). The
17
+ # lockfile records nothing but the relative remote, so the rebuilt source
18
+ # cannot tell which Gemfile that path was written in - but the Gemfile is
19
+ # always evaluated before the lockfile is parsed, so the copy that does know
20
+ # leaves the answer here for the copy that doesn't.
21
+ ROOT_PATHS = {}
22
+
23
+ # Bundler hands its own path source the directory that relative paths are
24
+ # resolved against, as "root_path", but it does not do so for plugin sources,
25
+ # so we work it out ourselves below. Accepting the option regardless means we
26
+ # pick it up for free if Bundler ever starts passing it.
27
+ def initialize(opts)
28
+ super
29
+ root_path = opts['root_path'] || declaring_gemfile_dir(caller_locations)
30
+ ROOT_PATHS[uri] = root_path if root_path
31
+ @root_path = ROOT_PATHS[uri] || root
32
+ end
33
+
34
+ # Called by Bundler once per gem that gets installed from this source.
35
+ #
36
+ # Note that `opts` are the install options passed in by Bundler (:build_args,
37
+ # :previous_spec, ...), whereas `options` are the source options that came
38
+ # from the Gemfile or the lockfile.
39
+ def install(spec, opts = {})
40
+ using_message = "Using #{version_message(spec, opts[:previous_spec])} from #{self}"
41
+ using_message += " and installing its executables" unless spec.executables.empty?
42
+ print_using_message using_message
43
+
44
+ # Bundler's installer would build the extensions into the regular extension
45
+ # directory, we build them ourselves below instead
46
+ generate_bin(spec, disable_extensions: true)
47
+
48
+ build_local_extensions(spec, opts[:build_args])
49
+
50
+ nil # no post-install message
51
+ end
52
+
53
+ # Bundler plugin api, we need to return a Bundler::Index
54
+ def specs
55
+ # A relative path in the Gemfile is relative to the Gemfile itself, and not
56
+ # to the directory bundler was run from
57
+ files = Dir.glob(File.join(File.expand_path(uri, @root_path), '*.gemspec'))
58
+
59
+ Bundler::Index.build do |index|
60
+ files.each do |file|
61
+ next unless spec = Bundler.load_gemspec(file)
62
+ spec.installed_by_version = Gem::VERSION
63
+ spec.source = self
64
+ spec.extension_dir = File.join(File.dirname(file), 'tmp', RUBY_PLATFORM, spec.name, RUBY_VERSION)
65
+ Bundler.rubygems.validate(spec)
66
+
67
+ index << spec
68
+ end
69
+ end
70
+ end
71
+
72
+ # The gems are always used in place, so there is nothing to fetch, cache or
73
+ # refresh here - all of these are intentional no-ops:
74
+ #
75
+ # local! - fetch the specs and install from the local system
76
+ # remote! - fetch the specs and install from a remote path
77
+ # cached! - fetch the specs and install from `app_cache_path`
78
+ # unlock! - refresh the cache/specs (e.g. git sources make a fresh clone)
79
+ def local!; end
80
+ def remote!; end
81
+ def cached!; end
82
+ def unlock!; end
83
+
84
+ private
85
+
86
+ # Bundler.root is the directory of the Gemfile bundler was pointed at, which
87
+ # is not necessarily the one that declared this source: a wrapper Gemfile
88
+ # that pulls the real one in through eval_gemfile (as the Ruby LSP and
89
+ # Appraisal do) moves the root into its own directory, while the source path
90
+ # still refers to the Gemfile it was written in.
91
+ #
92
+ # Bundler evaluates every Gemfile with `instance_eval(contents, gemfile_path)`,
93
+ # so while a source is being declared the Gemfile is the innermost frame
94
+ # outside of bundler, and its caller is bundler's DSL. Requiring both keeps
95
+ # an ordinary bundler stack from being mistaken for a Gemfile: a source
96
+ # rebuilt from the lockfile has no Gemfile frame, and falls back to the root.
97
+ def declaring_gemfile_dir(locations)
98
+ dsl = Bundler::Dsl.instance_method(:eval_gemfile).source_location.first
99
+ bundler_lib = File.dirname(File.dirname(dsl)) + File::SEPARATOR
100
+
101
+ index = locations.index { |location| !location.path.start_with?(bundler_lib) }
102
+ return if index.nil? || index.zero?
103
+ return unless locations[index - 1].path == dsl
104
+
105
+ File.dirname(File.expand_path(locations[index].path))
106
+ end
107
+
108
+ def build_local_extensions(spec, build_args = nil)
109
+ # Bundler passes in whatever "bundle config build.GEM" is set to, and the
110
+ # source options allow setting build args for all gems of a source
111
+ build_args = Array(build_args)
112
+ build_args = Array(options['build_args']) if build_args.empty?
113
+ build_args = Array(Bundler.rubygems.build_args) if build_args.empty?
114
+
115
+ builder = Gem::Ext::Builder.new spec, build_args
116
+
117
+ build_extensions(spec, build_args, builder)
118
+ end
119
+
120
+ def build_extensions(spec, build_args, builder)
121
+ return if spec.extensions.empty?
122
+
123
+ if build_args.empty?
124
+ Bundler.ui.info "Building native extensions for #{spec.name}. This could take a while..."
125
+ else
126
+ Bundler.ui.info "Building native extensions for #{spec.name} with: '#{build_args.join " "}'"
127
+ Bundler.ui.info "This could take a while..."
128
+ end
129
+
130
+ dest_path = spec.extension_dir
131
+ start = Time.now
132
+
133
+ spec.extensions.each do |extension|
134
+ builder_for_ext = if extension[/extconf/]
135
+ ExtConfAlwaysCopyBuilder
136
+ else
137
+ builder.builder_for(extension)
138
+ end
139
+
140
+ # This throws a Gem::Ext::BuildError if building the extension fails
141
+ build_extension spec, builder, builder_for_ext, extension, dest_path, build_args
142
+ end
143
+ Bundler.ui.info format(' Finished %s after %0.2f seconds', spec.name, Time.now - start)
144
+ FileUtils.touch(spec.gem_build_complete_path)
145
+ end
146
+
147
+ def build_extension(spec, builder, builder_for_ext, extension, dest_path, build_args) # :nodoc:
148
+ results = []
149
+
150
+ extension_dir =
151
+ File.expand_path File.join(spec.full_gem_path, File.dirname(extension))
152
+ lib_dir = File.join spec.full_gem_path, spec.raw_require_paths.first
153
+
154
+ begin
155
+ FileUtils.mkdir_p dest_path
156
+
157
+ results = builder_for_ext.build(extension, dest_path,
158
+ results, build_args, lib_dir, extension_dir)
159
+
160
+ builder.verbose { results.join("\n") }
161
+
162
+ builder.write_gem_make_out results.join "\n"
163
+ rescue StandardError => e
164
+ results << e.message
165
+ builder.build_error(results.join("\n"), $@)
166
+ end
167
+ end
168
+
169
+ def generate_bin(spec, options = {})
170
+ gem_dir = Pathname.new(spec.full_gem_path)
171
+
172
+ # Some gem authors put absolute paths in their gemspec
173
+ # and we have to save them from themselves
174
+ spec.files = spec.files.filter_map do |path|
175
+ pathname = Pathname.new(path)
176
+ next path unless pathname.absolute?
177
+ next if File.directory?(path)
178
+ begin
179
+ pathname.relative_path_from(gem_dir).to_s
180
+ rescue ArgumentError
181
+ path
182
+ end
183
+ end
184
+
185
+ installer = Bundler::Source::Path::Installer.new(
186
+ spec,
187
+ env_shebang: false,
188
+ disable_extensions: options[:disable_extensions],
189
+ build_args: options[:build_args],
190
+ bundler_extension_cache_path: extension_cache_path(spec)
191
+ )
192
+ installer.post_install
193
+ rescue Gem::InvalidSpecificationException => e
194
+ Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
195
+ "This prevents bundler from installing bins or native extensions, but " \
196
+ "that may not affect its functionality."
197
+
198
+ if !spec.extensions.empty? && !spec.email.empty?
199
+ Bundler.ui.warn "If you need to use this package without installing it from a gem " \
200
+ "repository, please contact #{spec.email} and ask them " \
201
+ "to modify their .gemspec so it can work with `gem build`."
202
+ end
203
+
204
+ Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}"
205
+ end
206
+ end
207
+
208
+ # Modified version of Gem::Ext::ExtConfBuilder that always copies the built binary
209
+ # to the destination path. This is necessary because when using local gems we cannot
210
+ # rely on versions being bumped when the gem changes.
211
+ class ExtConfAlwaysCopyBuilder < Gem::Ext::Builder
212
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
213
+ target_rbconfig = nil, n_jobs: nil)
214
+ require "fileutils"
215
+ require "tempfile"
216
+
217
+ target_rbconfig ||= Gem.target_rbconfig if HAS_TARGET_RBCONFIG
218
+
219
+ tmp_dest = Dir.mktmpdir(".gem.", extension_dir)
220
+
221
+ # Some versions of `mktmpdir` return absolute paths, which will break make
222
+ # if the paths contain spaces.
223
+ #
224
+ # As such, we convert to a relative path.
225
+ tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir)
226
+
227
+ destdir = ENV["DESTDIR"]
228
+
229
+ begin
230
+ cmd = ruby << File.basename(extension)
231
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if HAS_TARGET_RBCONFIG && target_rbconfig.path
232
+ cmd.push(*args)
233
+
234
+ run(cmd, results, 'ExtConfAlwaysCopy', extension_dir) do |s, r|
235
+ mkmf_log = File.join(extension_dir, "mkmf.log")
236
+ if File.exist? mkmf_log
237
+ unless s.success?
238
+ r << "To see why this extension failed to compile, please check" \
239
+ " the mkmf.log which can be found here:\n"
240
+ r << " " + File.join(dest_path, "mkmf.log") + "\n"
241
+ end
242
+ FileUtils.mv mkmf_log, dest_path
243
+ end
244
+ end
245
+
246
+ ENV["DESTDIR"] = nil
247
+
248
+ if HAS_TARGET_RBCONFIG && HAS_NJOBS
249
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
250
+ elsif HAS_TARGET_RBCONFIG
251
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig
252
+ else
253
+ make dest_path, results, extension_dir, tmp_dest_relative
254
+ end
255
+
256
+ full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
257
+
258
+ is_cross_compiling = HAS_TARGET_RBCONFIG && target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
259
+
260
+ entries = (Dir.entries(full_tmp_dest) - %w[. ..]).map {|entry| File.join full_tmp_dest, entry }
261
+
262
+ # MODIFIED
263
+ # Rubygems copies the built files into the gem's lib directory, and then
264
+ # additionally moves them into the extension install directory, but only
265
+ # the ones that don't exist there yet. We install into a single location,
266
+ # always overwriting, so a rebuilt extension leaves no stale binary behind.
267
+ #
268
+ # Note that install_extension_in_lib can be turned off through gemrc, and
269
+ # that Rubygems intends to eventually default it to false, which would
270
+ # make the extension directory the regular case rather than a fallback.
271
+ #
272
+ # Rubygems skips the copy into lib when cross-compiling, because that
273
+ # directory is shared with the build for the host platform, and relies on
274
+ # the extension install directory being specific to the target (it follows
275
+ # Gem::Platform.local, which is derived from the target rbconfig). The
276
+ # directory we use is keyed to the platform of the running Ruby, so we
277
+ # have nowhere to put a cross-compiled build, and skip it entirely.
278
+ unless is_cross_compiling
279
+ if Gem.install_extension_in_lib && lib_dir
280
+ FileUtils.mkdir_p lib_dir
281
+ FileUtils.cp_r entries, lib_dir, remove_destination: true
282
+ else
283
+ FileUtils.cp_r entries, dest_path, remove_destination: true
284
+ end
285
+ end
286
+
287
+ if HAS_TARGET_RBCONFIG
288
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
289
+ else
290
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"]
291
+ end
292
+ ensure
293
+ ENV["DESTDIR"] = destdir
294
+ end
295
+
296
+ results
297
+ rescue => error
298
+ if defined?(Gem::Ext::Builder::NoMakefileError) && error.is_a?(Gem::Ext::Builder::NoMakefileError)
299
+ results << error.message
300
+ results << "Skipping make for #{extension} as no Makefile was found."
301
+ # We are good, do not re-raise the error.
302
+ else
303
+ raise
304
+ end
305
+ ensure
306
+ FileUtils.rm_rf tmp_dest if tmp_dest
307
+ end
308
+
309
+ def self.get_relative_path(path, base)
310
+ path[0..base.length - 1] = "." if path.start_with?(base)
311
+ path
312
+ end
313
+ end
314
+ 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,23 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - pganalyze Team
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-01-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: ''
12
+ description: Bundler source plugin that works like a path source, but also builds
13
+ the native extensions of the gems it provides, like a remote gem source does.
14
14
  email:
15
15
  - team@pganalyze.com
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".github/workflows/test.yml"
20
21
  - ".gitignore"
21
22
  - Gemfile
22
23
  - Gemfile.lock
@@ -24,11 +25,12 @@ files:
24
25
  - README.md
25
26
  - Rakefile
26
27
  - bundler-source-pathext.gemspec
28
+ - lib/bundler-source-pathext.rb
27
29
  - plugins.rb
28
- homepage:
29
- licenses: []
30
+ homepage: https://github.com/pganalyze/bundler-path-build-ext
31
+ licenses:
32
+ - BSD-3-Clause
30
33
  metadata: {}
31
- post_install_message:
32
34
  rdoc_options: []
33
35
  require_paths:
34
36
  - lib
@@ -36,15 +38,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
38
  requirements:
37
39
  - - ">="
38
40
  - !ruby/object:Gem::Version
39
- version: 2.3.0
41
+ version: 2.7.0
40
42
  required_rubygems_version: !ruby/object:Gem::Requirement
41
43
  requirements:
42
44
  - - ">="
43
45
  - !ruby/object:Gem::Version
44
46
  version: '0'
45
47
  requirements: []
46
- rubygems_version: 3.5.22
47
- signing_key:
48
+ rubygems_version: 3.6.9
48
49
  specification_version: 4
49
- summary: ''
50
+ summary: Bundler source plugin for local gems with native extensions
50
51
  test_files: []