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 +4 -4
- data/.github/workflows/test.yml +34 -0
- data/README.md +24 -4
- data/Rakefile +7 -1
- data/bundler-source-pathext.gemspec +7 -4
- data/lib/bundler-source-pathext.rb +271 -0
- data/plugins.rb +7 -264
- metadata +11 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9746323d874d99471d27cbbec67b46d871fc5076327908f4e6165ad77f61961
|
|
4
|
+
data.tar.gz: 7fb084483775bd60058988c143ec08278f334d1db030db17870c0ec0a3cc87b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
1
|
+
# bundler-source-pathext
|
|
2
|
+
[ ](https://rubygems.org/gems/bundler-source-pathext)
|
|
3
|
+
[ ](https://rubygems.org/gems/bundler-source-pathext)
|
|
4
|
+
[ ](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,
|
|
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
|
|
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
|
|
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,12 +1,15 @@
|
|
|
1
1
|
Gem::Specification.new do |spec|
|
|
2
2
|
spec.name = "bundler-source-pathext"
|
|
3
|
-
spec.version = "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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
def install(spec, opts)
|
|
7
|
-
print_using_message "Using #{spec.name} #{spec.version} from #{self}"
|
|
3
|
+
require 'bundler-source-pathext'
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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: []
|