Skip to content

Commit f927f51

Browse files
committed
Raise a more descriptive error message for pg_ext LoadError
If the `3.4/pg_ext.so` doesn't load, the specific error message is overwritten by the subsequent LoadError of `pg_ext.so`. This is bad since this can hide valuable information about the load error, like a missing dependency. One such mussing dependency can be the required GLIBC version. This patch adds a more descriptive error message containing both attempts to load the C extension. And it adds an additional help text for the special case of a GLIBC load error. The old error: ``` cannot load such file -- pg_ext (LoadError) ``` The new error: ``` pg's C extension failed to load: (LoadError) /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by /root/.rubies/ruby-3.2.9/lib/ruby/gems/3.2.0/gems/pg-1.6.1-x86_64-linux/lib/3.2/pg_ext.so) - /root/.rubies/ruby-3.2.9/lib/ruby/gems/3.2.0/gems/pg-1.6.1-x86_64-linux/lib/3.2/pg_ext.so cannot load such file -- pg_ext The GLIBC version of this system seems too old. Please use the source version of pg: gem uninstall pg --all gem install pg --platform ruby or in your Gemfile: gem "pg", force_ruby_platform: true See also: https://github.com/ged/ruby-pg/blob/master/README.md#source-gem ``` Changing from tabs to spaces for better message formatting. Relates to ged#661
1 parent dae9173 commit f927f51

File tree

5 files changed

+213
-136
lines changed

5 files changed

+213
-136
lines changed

.github/workflows/binary-gems.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,26 @@ jobs:
223223
cp -v pg-*.gem misc/yugabyte/
224224
cd misc/yugabyte
225225
docker-compose up --abort-on-container-exit --exit-code-from pg
226+
227+
job_binary_too_old_glibc:
228+
name: GLIBC
229+
needs: rcd_build
230+
strategy:
231+
fail-fast: false
232+
matrix:
233+
include:
234+
- gem_platform: x86_64-linux
235+
236+
runs-on: ubuntu-latest
237+
steps:
238+
- uses: actions/checkout@v4
239+
- name: Download gem-${{ matrix.gem_platform }}
240+
uses: actions/download-artifact@v4
241+
with:
242+
name: binary-gem-${{ matrix.gem_platform }}
243+
- name: Build image and Run tests
244+
run: |
245+
sudo apt-get install -y docker-compose
246+
cp -v pg-*.gem misc/glibc/
247+
cd misc/glibc
248+
docker-compose up --abort-on-container-exit --exit-code-from pg

lib/pg.rb

Lines changed: 156 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -5,141 +5,161 @@
55
# The top-level PG namespace.
66
module PG
77

8-
# Is this file part of a fat binary gem with bundled libpq?
9-
# This path must be enabled by add_dll_directory on Windows.
10-
gplat = Gem::Platform.local
11-
bundled_libpq_path = Dir[File.expand_path("../ports/#{gplat.cpu}-#{gplat.os}*/lib", __dir__)].first
12-
if bundled_libpq_path
13-
POSTGRESQL_LIB_PATH = bundled_libpq_path
14-
else
15-
# Try to load libpq path as found by extconf.rb
16-
begin
17-
require "pg/postgresql_lib_path"
18-
rescue LoadError
19-
# rake-compiler doesn't use regular "make install", but uses it's own install tasks.
20-
# It therefore doesn't copy pg/postgresql_lib_path.rb in case of "rake compile".
21-
POSTGRESQL_LIB_PATH = false
22-
end
23-
end
24-
POSTGRESQL_LIB_PATH.freeze
25-
26-
add_dll_path = proc do |path, &block|
27-
if RUBY_PLATFORM =~/(mswin|mingw)/i && path
28-
BUNDLED_LIBPQ_WITH_UNIXSOCKET = false
29-
begin
30-
require 'ruby_installer/runtime'
31-
RubyInstaller::Runtime.add_dll_directory(path, &block)
32-
rescue LoadError
33-
old_path = ENV['PATH']
34-
ENV['PATH'] = "#{path};#{old_path}"
35-
block.call
36-
ENV['PATH'] = old_path
37-
end
38-
else
39-
# libpq is found by a relative rpath in the cross compiled extension dll
40-
# or by the system library loader
41-
block.call
42-
BUNDLED_LIBPQ_WITH_UNIXSOCKET = RUBY_PLATFORM=~/linux/i && PG::IS_BINARY_GEM
43-
end
44-
end
45-
46-
# Add a load path to the one retrieved from pg_config
47-
add_dll_path.call(POSTGRESQL_LIB_PATH) do
48-
begin
49-
# Try the <major>.<minor> subdirectory for fat binary gems
50-
major_minor = RUBY_VERSION[ /^(\d+\.\d+)/ ] or
51-
raise "Oops, can't extract the major/minor version from #{RUBY_VERSION.dump}"
52-
require "#{major_minor}/pg_ext"
53-
rescue LoadError
54-
require 'pg_ext'
55-
end
56-
end
57-
58-
# Get the PG library version.
59-
#
60-
# +include_buildnum+ is no longer used and any value passed will be ignored.
61-
def self.version_string( include_buildnum=nil )
62-
"%s %s" % [ self.name, VERSION ]
63-
end
64-
65-
66-
### Convenience alias for PG::Connection.new.
67-
def self.connect( *args, &block )
68-
Connection.new( *args, &block )
69-
end
70-
71-
if defined?(Ractor.make_shareable)
72-
def self.make_shareable(obj)
73-
Ractor.make_shareable(obj)
74-
end
75-
else
76-
def self.make_shareable(obj)
77-
obj.freeze
78-
end
79-
end
80-
81-
module BinaryDecoder
82-
%i[ TimestampUtc TimestampUtcToLocal TimestampLocal ].each do |klass|
83-
autoload klass, 'pg/binary_decoder/timestamp'
84-
end
85-
autoload :Date, 'pg/binary_decoder/date'
86-
end
87-
module BinaryEncoder
88-
%i[ TimestampUtc TimestampLocal ].each do |klass|
89-
autoload klass, 'pg/binary_encoder/timestamp'
90-
end
91-
end
92-
module TextDecoder
93-
%i[ TimestampUtc TimestampUtcToLocal TimestampLocal TimestampWithoutTimeZone TimestampWithTimeZone ].each do |klass|
94-
autoload klass, 'pg/text_decoder/timestamp'
95-
end
96-
autoload :Date, 'pg/text_decoder/date'
97-
autoload :Inet, 'pg/text_decoder/inet'
98-
autoload :JSON, 'pg/text_decoder/json'
99-
autoload :Numeric, 'pg/text_decoder/numeric'
100-
end
101-
module TextEncoder
102-
%i[ TimestampUtc TimestampWithoutTimeZone TimestampWithTimeZone ].each do |klass|
103-
autoload klass, 'pg/text_encoder/timestamp'
104-
end
105-
autoload :Date, 'pg/text_encoder/date'
106-
autoload :Inet, 'pg/text_encoder/inet'
107-
autoload :JSON, 'pg/text_encoder/json'
108-
autoload :Numeric, 'pg/text_encoder/numeric'
109-
end
110-
111-
autoload :BasicTypeMapBasedOnResult, 'pg/basic_type_map_based_on_result'
112-
autoload :BasicTypeMapForQueries, 'pg/basic_type_map_for_queries'
113-
autoload :BasicTypeMapForResults, 'pg/basic_type_map_for_results'
114-
autoload :BasicTypeRegistry, 'pg/basic_type_registry'
115-
require 'pg/exceptions'
116-
require 'pg/coder'
117-
require 'pg/type_map_by_column'
118-
require 'pg/connection'
119-
require 'pg/cancel_connection'
120-
require 'pg/result'
121-
require 'pg/tuple'
122-
autoload :VERSION, 'pg/version'
123-
124-
125-
# Avoid "uninitialized constant Truffle::WarningOperations" on Truffleruby up to 22.3.1
126-
if RUBY_ENGINE=="truffleruby" && !defined?(Truffle::WarningOperations)
127-
module TruffleFixWarn
128-
def warn(str, category=nil)
129-
super(str)
130-
end
131-
end
132-
Warning.extend(TruffleFixWarn)
133-
end
134-
135-
# Ruby-3.4+ prints a warning, if bigdecimal is required but not in the Gemfile.
136-
# But it's a false positive, since we enable bigdecimal depending features only if it's available.
137-
# And most people don't need these features.
138-
def self.require_bigdecimal_without_warning
139-
oldverb, $VERBOSE = $VERBOSE, nil
140-
require "bigdecimal"
141-
ensure
142-
$VERBOSE = oldverb
143-
end
8+
# Is this file part of a fat binary gem with bundled libpq?
9+
# This path must be enabled by add_dll_directory on Windows.
10+
gplat = Gem::Platform.local
11+
bundled_libpq_path = Dir[File.expand_path("../ports/#{gplat.cpu}-#{gplat.os}*/lib", __dir__)].first
12+
if bundled_libpq_path
13+
POSTGRESQL_LIB_PATH = bundled_libpq_path
14+
else
15+
# Try to load libpq path as found by extconf.rb
16+
begin
17+
require "pg/postgresql_lib_path"
18+
rescue LoadError
19+
# rake-compiler doesn't use regular "make install", but uses it's own install tasks.
20+
# It therefore doesn't copy pg/postgresql_lib_path.rb in case of "rake compile".
21+
POSTGRESQL_LIB_PATH = false
22+
end
23+
end
24+
POSTGRESQL_LIB_PATH.freeze
25+
26+
add_dll_path = proc do |path, &block|
27+
if RUBY_PLATFORM =~/(mswin|mingw)/i && path
28+
BUNDLED_LIBPQ_WITH_UNIXSOCKET = false
29+
begin
30+
require 'ruby_installer/runtime'
31+
RubyInstaller::Runtime.add_dll_directory(path, &block)
32+
rescue LoadError
33+
old_path = ENV['PATH']
34+
ENV['PATH'] = "#{path};#{old_path}"
35+
block.call
36+
ENV['PATH'] = old_path
37+
end
38+
else
39+
# libpq is found by a relative rpath in the cross compiled extension dll
40+
# or by the system library loader
41+
block.call
42+
BUNDLED_LIBPQ_WITH_UNIXSOCKET = RUBY_PLATFORM=~/linux/i && PG::IS_BINARY_GEM
43+
end
44+
end
45+
46+
# Add a load path to the one retrieved from pg_config
47+
add_dll_path.call(POSTGRESQL_LIB_PATH) do
48+
begin
49+
# Try the <major>.<minor> subdirectory for fat binary gems
50+
major_minor = RUBY_VERSION[ /^(\d+\.\d+)/ ] or
51+
raise "Oops, can't extract the major/minor version from #{RUBY_VERSION.dump}"
52+
require "#{major_minor}/pg_ext"
53+
rescue LoadError => error1
54+
begin
55+
require 'pg_ext'
56+
rescue LoadError => error2
57+
msg = <<~EOT
58+
pg's C extension failed to load:
59+
#{error1}
60+
#{error2}
61+
EOT
62+
if msg =~ /GLIBC/
63+
msg += <<~EOT
64+
65+
The GLIBC version of this system seems too old. Please use the source version of pg:
66+
gem uninstall pg --all
67+
gem install pg --platform ruby
68+
or in your Gemfile:
69+
gem "pg", force_ruby_platform: true
70+
See also: https://deveiate.org/code/pg/README_md.html#label-Source+gem
71+
EOT
72+
end
73+
raise error2, msg
74+
end
75+
end
76+
end
77+
78+
# Get the PG library version.
79+
#
80+
# +include_buildnum+ is no longer used and any value passed will be ignored.
81+
def self.version_string( include_buildnum=nil )
82+
"%s %s" % [ self.name, VERSION ]
83+
end
84+
85+
86+
### Convenience alias for PG::Connection.new.
87+
def self.connect( *args, &block )
88+
Connection.new( *args, &block )
89+
end
90+
91+
if defined?(Ractor.make_shareable)
92+
def self.make_shareable(obj)
93+
Ractor.make_shareable(obj)
94+
end
95+
else
96+
def self.make_shareable(obj)
97+
obj.freeze
98+
end
99+
end
100+
101+
module BinaryDecoder
102+
%i[ TimestampUtc TimestampUtcToLocal TimestampLocal ].each do |klass|
103+
autoload klass, 'pg/binary_decoder/timestamp'
104+
end
105+
autoload :Date, 'pg/binary_decoder/date'
106+
end
107+
module BinaryEncoder
108+
%i[ TimestampUtc TimestampLocal ].each do |klass|
109+
autoload klass, 'pg/binary_encoder/timestamp'
110+
end
111+
end
112+
module TextDecoder
113+
%i[ TimestampUtc TimestampUtcToLocal TimestampLocal TimestampWithoutTimeZone TimestampWithTimeZone ].each do |klass|
114+
autoload klass, 'pg/text_decoder/timestamp'
115+
end
116+
autoload :Date, 'pg/text_decoder/date'
117+
autoload :Inet, 'pg/text_decoder/inet'
118+
autoload :JSON, 'pg/text_decoder/json'
119+
autoload :Numeric, 'pg/text_decoder/numeric'
120+
end
121+
module TextEncoder
122+
%i[ TimestampUtc TimestampWithoutTimeZone TimestampWithTimeZone ].each do |klass|
123+
autoload klass, 'pg/text_encoder/timestamp'
124+
end
125+
autoload :Date, 'pg/text_encoder/date'
126+
autoload :Inet, 'pg/text_encoder/inet'
127+
autoload :JSON, 'pg/text_encoder/json'
128+
autoload :Numeric, 'pg/text_encoder/numeric'
129+
end
130+
131+
autoload :BasicTypeMapBasedOnResult, 'pg/basic_type_map_based_on_result'
132+
autoload :BasicTypeMapForQueries, 'pg/basic_type_map_for_queries'
133+
autoload :BasicTypeMapForResults, 'pg/basic_type_map_for_results'
134+
autoload :BasicTypeRegistry, 'pg/basic_type_registry'
135+
require 'pg/exceptions'
136+
require 'pg/coder'
137+
require 'pg/type_map_by_column'
138+
require 'pg/connection'
139+
require 'pg/cancel_connection'
140+
require 'pg/result'
141+
require 'pg/tuple'
142+
autoload :VERSION, 'pg/version'
143+
144+
145+
# Avoid "uninitialized constant Truffle::WarningOperations" on Truffleruby up to 22.3.1
146+
if RUBY_ENGINE=="truffleruby" && !defined?(Truffle::WarningOperations)
147+
module TruffleFixWarn
148+
def warn(str, category=nil)
149+
super(str)
150+
end
151+
end
152+
Warning.extend(TruffleFixWarn)
153+
end
154+
155+
# Ruby-3.4+ prints a warning, if bigdecimal is required but not in the Gemfile.
156+
# But it's a false positive, since we enable bigdecimal depending features only if it's available.
157+
# And most people don't need these features.
158+
def self.require_bigdecimal_without_warning
159+
oldverb, $VERBOSE = $VERBOSE, nil
160+
require "bigdecimal"
161+
ensure
162+
$VERBOSE = oldverb
163+
end
144164

145165
end # module PG

misc/glibc/Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM debian:10.13
2+
3+
WORKDIR /pg
4+
5+
# Debian 10.13 is EOL now:
6+
RUN sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list
7+
8+
RUN apt-get update && apt-get install ruby git wget gcc make libz-dev libffi-dev libreadline-dev libyaml-dev libssl-dev -y
9+
10+
ENV RBENV_ROOT=/usr/local/rbenv
11+
12+
RUN git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
13+
git clone https://github.com/rbenv/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build && \
14+
$RBENV_ROOT/bin/rbenv init
15+
16+
RUN $RBENV_ROOT/bin/rbenv install 3.3.9 -- --disable-install-doc
17+
RUN /usr/local/rbenv/versions/3.3.9/bin/gem inst rspec
18+
19+
CMD /usr/local/rbenv/versions/3.3.9/bin/gem inst --local pg-*.gem && \
20+
/usr/local/rbenv/versions/3.3.9/bin/rspec glibc_spec.rb

misc/glibc/docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
services:
2+
pg:
3+
build:
4+
context: .
5+
args:
6+
- http_proxy
7+
- https_proxy
8+
volumes:
9+
- .:/pg

misc/glibc/glibc_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RSpec.describe "require 'pg'" do
2+
it "gives a descriptive error message when GLIBC is too old" do
3+
expect { require "pg" }.to raise_error(/GLIBC.*gem install pg --platform ruby/m)
4+
end
5+
end

0 commit comments

Comments
 (0)