From f6ccdae9db2375716d0d0dfd55e9cc54ca1cf141 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Wed, 6 Jan 2021 18:03:37 +0100 Subject: [PATCH 1/2] Make the library path of libpq available in ruby ... and add it to the search paths on Windows which lacks rpath. In ELF based systems the libpq path is stored as rpath in pg_ext.so, so that libpq is found, even if it's not in the library search paths. On Windows however the rpath mechanism doesn't work and the search path for libpq must be added manually. This is done per RubyInstaller::Runtime.add_dll_directory. The libpq path is available in extconf.rb while build only (while gem install), so that we have to pass the string to lib/pg.rb where pg_ext.so is loaded (while require 'pg'). This is a bit tricky, since pg_ext.so is linked to libpq, so that the library search path has to be set first, prior to loading pg_ext.so. Therefore a second file is needed bypassing pg_ext.so. That is postgresql_lib_path.rb and it is passed by mkmf's $INSTALLFILES mechanism. There's an issue with rake-compiler, which (in contrast to gem install) doesn't copy postgresql_lib_path.rb. This has to be addressed separatelly. Another approach would be to add a second extension file, but that is more complicated. Fixes #365 --- Rakefile | 1 + appveyor.yml | 1 - ext/extconf.rb | 28 ++++++++++++++++++++------- lib/pg.rb | 51 ++++++++++++++++++++++++++++++++----------------- spec/pg_spec.rb | 5 ++++- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/Rakefile b/Rakefile index 58a8f1127..a81aaf595 100644 --- a/Rakefile +++ b/Rakefile @@ -37,6 +37,7 @@ CLOBBER.include( TESTDIR.to_s ) CLEAN.include( PKGDIR.to_s, TMPDIR.to_s ) CLEAN.include "lib/*/libpq.dll" CLEAN.include "lib/pg_ext.*" +CLEAN.include "lib/pg/postgresql_lib_path.rb" # Set up Hoe plugins Hoe.plugin :mercurial diff --git a/appveyor.yml b/appveyor.yml index 345236e4f..3cd9de761 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,6 @@ install: $(new-object net.webclient).DownloadFile('http://get.enterprisedb.com/postgresql/postgresql-' + $env:PGVERSION + '.exe', 'C:/postgresql-setup.exe') cmd /c "C:/postgresql-setup.exe" --mode unattended --extract-only 1 } - $env:RUBY_DLL_PATH = 'C:/Program Files/PostgreSQL/' + $env:PGVER + '/bin;C:/Program Files (x86)/PostgreSQL/' + $env:PGVER + '/bin' $env:PATH = 'C:/Program Files/PostgreSQL/' + $env:PGVER + '/bin;' + $env:PATH $env:PATH = 'C:/Program Files (x86)/PostgreSQL/' + $env:PGVER + '/bin;' + $env:PATH build_script: diff --git a/ext/extconf.rb b/ext/extconf.rb index beced6e79..9ea7469b9 100755 --- a/ext/extconf.rb +++ b/ext/extconf.rb @@ -34,19 +34,33 @@ libdir = `"#{pgconfig}" --libdir`.chomp dir_config 'pg', incdir, libdir - # Try to use runtime path linker option, even if RbConfig doesn't know about it. - # The rpath option is usually set implicit by dir_config(), but so far not - # on MacOS-X. - if RbConfig::CONFIG["RPATHFLAG"].to_s.empty? && try_link('int main() {return 0;}', " -Wl,-rpath,#{libdir}") - $LDFLAGS << " -Wl,-rpath,#{libdir}" - end + # Windows traditionally stores DLLs beside executables, not in libdir + dlldir = RUBY_PLATFORM=~/mingw|mswin/ ? `"#{pgconfig}" --bindir`.chomp : libdir + else $stderr.puts "No pg_config... trying anyway. If building fails, please try again with", " --with-pg-config=/path/to/pg_config" - dir_config 'pg' + incdir, libdir = dir_config 'pg' + dlldir = libdir + end + + # Try to use runtime path linker option, even if RbConfig doesn't know about it. + # The rpath option is usually set implicit by dir_config(), but so far not + # on MacOS-X. + if dlldir && RbConfig::CONFIG["RPATHFLAG"].to_s.empty? + append_ldflags "-Wl,-rpath,#{dlldir.quote}" end end +File.write("postgresql_lib_path.rb", <<-EOT) +module PG + POSTGRESQL_LIB_PATH = #{dlldir.inspect} +end +EOT +$INSTALLFILES = { + "./postgresql_lib_path.rb" => "$(RUBYLIBDIR)/pg/" +} + if RUBY_VERSION >= '2.3.0' && /solaris/ =~ RUBY_PLATFORM append_cppflags( '-D__EXTENSIONS__' ) end diff --git a/lib/pg.rb b/lib/pg.rb index a53554670..9414d8719 100644 --- a/lib/pg.rb +++ b/lib/pg.rb @@ -1,15 +1,28 @@ + # -*- ruby -*- # frozen_string_literal: true -begin - require 'pg_ext' -rescue LoadError - # If it's a Windows binary gem, try the . subdirectory - if RUBY_PLATFORM =~/(mswin|mingw)/i - major_minor = RUBY_VERSION[ /^(\d+\.\d+)/ ] or - raise "Oops, can't extract the major/minor version from #{RUBY_VERSION.dump}" +# The top-level PG namespace. +module PG + + # Is this file part of a fat binary gem with bundled libpq? + bundled_libpq_path = File.join(__dir__, RUBY_PLATFORM.gsub(/^i386-/, "x86-")) + if File.exist?(bundled_libpq_path) + POSTGRESQL_LIB_PATH = bundled_libpq_path + else + bundled_libpq_path = nil + # Try to load libpq path as found by extconf.rb + begin + require "pg/postgresql_lib_path" + rescue LoadError + # rake-compiler doesn't use regular "make install", but uses it's own install tasks. + # It therefore doesn't copy pg/postgresql_lib_path.rb in case of "rake compile". + POSTGRESQL_LIB_PATH = false + end + end - add_dll_path = proc do |path, &block| + add_dll_path = proc do |path, &block| + if RUBY_PLATFORM =~/(mswin|mingw)/i && path && File.exist?(path) begin require 'ruby_installer/runtime' RubyInstaller::Runtime.add_dll_directory(path, &block) @@ -19,22 +32,24 @@ block.call ENV['PATH'] = old_path end + else + # No need to set a load path manually - it's set as library rpath. + block.call end + end - # Temporary add this directory for DLL search, so that libpq.dll can be found. - # mingw32-platform strings differ (RUBY_PLATFORM=i386-mingw32 vs. x86-mingw32 for rubygems) - add_dll_path.call(File.join(__dir__, RUBY_PLATFORM.gsub(/^i386-/, "x86-"))) do + # Add a load path to the one retrieved from pg_config + add_dll_path.call(POSTGRESQL_LIB_PATH) do + if bundled_libpq_path + # It's a Windows binary gem, try the . subdirectory + major_minor = RUBY_VERSION[ /^(\d+\.\d+)/ ] or + raise "Oops, can't extract the major/minor version from #{RUBY_VERSION.dump}" require "#{major_minor}/pg_ext" + else + require 'pg_ext' end - else - raise end -end - - -# The top-level PG namespace. -module PG # Library version VERSION = '1.2.3' diff --git a/spec/pg_spec.rb b/spec/pg_spec.rb index 9f9f484cd..6b291b0ed 100644 --- a/spec/pg_spec.rb +++ b/spec/pg_spec.rb @@ -46,5 +46,8 @@ ]) end -end + it "tells about the libpq library path" do + expect( PG::POSTGRESQL_LIB_PATH ).to include("/") + end +end From 2f6ab32aafba1159128e4aa5c6f49caaffc5f847 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 16 Mar 2021 13:17:33 +0100 Subject: [PATCH 2/2] Add patch to rake-compiler-1.1.1 for make install With this patch rake-compiler executes not just "make" but also "make install" the same way as "gem install" does. That way postgresql_lib_path.rb is copied into the lib folder as expected. --- Gemfile | 2 +- Rakefile | 3 +- misc/rake-compiler-make-install-patch.rb | 153 +++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 misc/rake-compiler-make-install-patch.rb diff --git a/Gemfile b/Gemfile index db6adc43a..5da8bb808 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ source "https://rubygems.org/" gem "hoe-mercurial", "~>1.4", :group => [:development, :test] gem "hoe-deveiate", "~>0.9", :group => [:development, :test] gem "hoe-highline", "~>0.2", :group => [:development, :test] -gem "rake-compiler", "~>1.0", :group => [:development, :test] +gem "rake-compiler", "1.1.1", :group => [:development, :test] gem "rake-compiler-dock", "~>1.0", :group => [:development, :test] gem "hoe-bundler", "~>1.0", :group => [:development, :test] gem "rspec", "~>3.5", :group => [:development, :test] diff --git a/Rakefile b/Rakefile index a81aaf595..5722c3f55 100644 --- a/Rakefile +++ b/Rakefile @@ -6,6 +6,7 @@ require 'tmpdir' begin require 'rake/extensiontask' + require_relative 'misc/rake-compiler-make-install-patch' rescue LoadError abort "This Rakefile requires rake-compiler (gem install rake-compiler)" end @@ -63,7 +64,7 @@ $hoespec = Hoe.spec 'pg' do self.developer 'Michael Granger', 'ged@FaerieMUD.org' self.developer 'Lars Kanis', 'lars@greiz-reinsdorf.de' - self.dependency 'rake-compiler', '~> 1.0', :developer + self.dependency 'rake-compiler', '1.1.1', :developer self.dependency 'rake-compiler-dock', ['~> 1.0'], :developer self.dependency 'hoe-deveiate', '~> 0.9', :developer self.dependency 'hoe-bundler', '~> 1.0', :developer diff --git a/misc/rake-compiler-make-install-patch.rb b/misc/rake-compiler-make-install-patch.rb new file mode 100644 index 000000000..20a4cfb1b --- /dev/null +++ b/misc/rake-compiler-make-install-patch.rb @@ -0,0 +1,153 @@ +require 'rake/baseextensiontask' + +module Rake + class ExtensionTask < BaseExtensionTask + + # Replace method + undef define_compile_tasks + + def define_compile_tasks(for_platform = nil, ruby_ver = RUBY_VERSION) + # platform usage + platf = for_platform || platform + + binary_path = binary(platf) + + # lib_path + lib_path = lib_dir + + lib_binary_path = "#{lib_path}/#{binary_path}" + lib_binary_dir_path = File.dirname(lib_binary_path) + + # tmp_path + tmp_path = "#{@tmp_dir}/#{platf}/#{@name}/#{ruby_ver}" + stage_path = "#{@tmp_dir}/#{platf}/stage" + + siteconf_path = "#{tmp_path}/.rake-compiler-siteconf.rb" + tmp_binary_path = "#{tmp_path}/#{binary_path}" + tmp_binary_dir_path = File.dirname(tmp_binary_path) + stage_binary_path = "#{stage_path}/#{lib_path}/#{binary_path}" + stage_binary_dir_path = File.dirname(stage_binary_path) + + # cleanup and clobbering + CLEAN.include(tmp_path) + CLEAN.include(stage_path) + CLOBBER.include("#{lib_path}/#{binary(platf)}") + CLOBBER.include("#{@tmp_dir}") + + # directories we need + directory tmp_path + directory tmp_binary_dir_path + directory lib_binary_dir_path + directory stage_binary_dir_path + + directory File.dirname(siteconf_path) + # Set paths for "make install" destinations + file siteconf_path => File.dirname(siteconf_path) do + File.open(siteconf_path, "w") do |siteconf| + siteconf.puts "require 'rbconfig'" + siteconf.puts "require 'mkmf'" + siteconf.puts "dest_path = mkintpath(#{File.expand_path(lib_path).dump})" + %w[sitearchdir sitelibdir].each do |dir| + siteconf.puts "RbConfig::MAKEFILE_CONFIG['#{dir}'] = dest_path" + siteconf.puts "RbConfig::CONFIG['#{dir}'] = dest_path" + end + end + end + + # copy binary from temporary location to final lib + # tmp/extension_name/extension_name.{so,bundle} => lib/ + task "copy:#{@name}:#{platf}:#{ruby_ver}" => [lib_binary_dir_path, tmp_binary_path, "#{tmp_path}/Makefile"] do + # install in lib for native platform only + unless for_platform + sh "#{make} install", chdir: tmp_path + end + end + # copy binary from temporary location to staging directory + task "copy:#{@name}:#{platf}:#{ruby_ver}" => [stage_binary_dir_path, tmp_binary_path] do + cp tmp_binary_path, stage_binary_path + end + + # copy other gem files to staging directory + define_staging_file_tasks(@gem_spec.files, lib_path, stage_path, platf, ruby_ver) if @gem_spec + + # binary in temporary folder depends on makefile and source files + # tmp/extension_name/extension_name.{so,bundle} + file tmp_binary_path => [tmp_binary_dir_path, "#{tmp_path}/Makefile"] + source_files do + jruby_compile_msg = <<-EOF +Compiling a native C extension on JRuby. This is discouraged and a +Java extension should be preferred. + EOF + warn_once(jruby_compile_msg) if defined?(JRUBY_VERSION) + + chdir tmp_path do + sh make + if binary_path != File.basename(binary_path) + cp File.basename(binary_path), binary_path + end + end + end + + # makefile depends of tmp_dir and config_script + # tmp/extension_name/Makefile + file "#{tmp_path}/Makefile" => [tmp_path, extconf, siteconf_path] do |t| + options = @config_options.dup + + # include current directory + include_dirs = ['.'].concat(@config_includes).uniq.join(File::PATH_SEPARATOR) + cmd = [Gem.ruby, "-I#{include_dirs}", "-r#{File.basename(siteconf_path)}"] + + # build a relative path to extconf script + abs_tmp_path = (Pathname.new(Dir.pwd) + tmp_path).realpath + abs_extconf = (Pathname.new(Dir.pwd) + extconf).realpath + + # now add the extconf script + cmd << abs_extconf.relative_path_from(abs_tmp_path) + + # fake.rb will be present if we are cross compiling + if t.prerequisites.include?("#{tmp_path}/fake.rb") then + options.push(*cross_config_options(platf)) + end + + # add options to command + cmd.push(*options) + + # add any extra command line options + unless extra_options.empty? + cmd.push(*extra_options) + end + + chdir tmp_path do + # FIXME: Rake is broken for multiple arguments system() calls. + # Add current directory to the search path of Ruby + sh cmd.join(' ') + end + end + + # compile tasks + unless Rake::Task.task_defined?('compile') then + desc "Compile all the extensions" + task "compile" + end + + # compile:name + unless Rake::Task.task_defined?("compile:#{@name}") then + desc "Compile #{@name}" + task "compile:#{@name}" + end + + # Allow segmented compilation by platform (open door for 'cross compile') + task "compile:#{@name}:#{platf}" => ["copy:#{@name}:#{platf}:#{ruby_ver}"] + task "compile:#{platf}" => ["compile:#{@name}:#{platf}"] + + # Only add this extension to the compile chain if current + # platform matches the indicated one. + if platf == RUBY_PLATFORM then + # ensure file is always copied + file "#{lib_path}/#{binary_path}" => ["copy:#{name}:#{platf}:#{ruby_ver}"] + + task "compile:#{@name}" => ["compile:#{@name}:#{platf}"] + task "compile" => ["compile:#{platf}"] + end + end + end +end