Родные расширения возвращаются в чистый Ruby, если не поддерживаются при установке gem

Я разрабатываю драгоценный камень, который в настоящее время является чистым Ruby, но я также разрабатываю более быстрый вариант C для одной из функций. Функция доступна, но иногда медленная, в чистом Ruby. Медленность будет влиять только на некоторых потенциальных пользователей (зависит от того, какие функции им нужны и как они их используют), поэтому имеет смысл иметь доступный камень с грациозным откатом для функций Ruby, если он не может скомпилироваться в целевой системе.

Я хотел бы сохранить варианты Ruby и C этой функции в одном драгоценном камне и обеспечить лучший (то есть самый быстрый) опыт от жемчужины при установке. Это позволило бы мне поддерживать самый широкий набор потенциальных пользователей из одного моего проекта. Это также позволит другим людям, зависимым от камней и проектов, использовать наилучшую доступную зависимость от целевой системы, в отличие от версии с наименьшим общим знаменателем для совместимости.

Я ожидал, что require вернется во время выполнения, чтобы он появился в основном файле lib/foo.rb, просто так:

begin
  require 'foo/foo_extended'
rescue LoadError
  require 'foo/ext_bits_as_pure_ruby'
end

Однако я не знаю, как заставить установку gem проверять (или пытаться и терпеть неудачу) на встроенную поддержку расширения, чтобы gem правильно установил, может ли он построить foo_extended. Когда я исследовал, как это сделать, я в основном нашел обсуждения с нескольких лет назад, например. http://permalink.gmane.org/gmane.comp.lang.ruby.gems.devel/1479 и http://rubyforge.org/pipermail/rubygems-developers/2007-November/003220.html, которые подразумевают Ruby драгоценные камни действительно не поддерживают эту функцию. Ничего недавнего, так что я надеюсь, что кто-то из SO будет иметь некоторые более современные знания?

Мое идеальное решение было бы способом обнаружить, прежде чем пытаться построить расширение, чтобы целевой Ruby не поддерживал (или, возможно, просто не хотел, на уровне проекта) родные расширения C. Но также, механизм try/catch будет в порядке, если не слишком грязный.

Возможно ли это, если да, то как? Или совет, чтобы опубликовать два варианта драгоценных камней (например, foo и foo_ruby), которые я нахожу при поиске, по-прежнему актуальной лучшей практике?

Ответ 1

Это мой лучший результат, пытаясь ответить на мой собственный вопрос на сегодняшний день. Похоже, что он работает для JRuby (тестировался в Travis и моей локальной установке под RVM), что было моей главной целью. Тем не менее, я был бы очень заинтересован в подтверждении того, что он работает в других средах, и для любого ввода информации о том, как сделать его более общим и/или надежным:


Код установки gem ожидает Makefile как вывод из extconf.rb, но не имеет никакого мнения о том, что должно содержать. Поэтому extconf.rb может решить создать ничего Makefile, вместо вызова create_makefile из mkmf. На практике это может выглядеть так:

внутр /Foo/extconf.rb

can_compile_extensions = false
want_extensions = true

begin
  require 'mkmf'
  can_compile_extensions = true
rescue Exception
  # This will appear only in verbose mode.
  $stderr.puts "Could not require 'mkmf'. Not fatal, the extensions are optional."
end


if can_compile_extensions && want_extensions
  create_makefile( 'foo/foo' )

else
  # Create a dummy Makefile, to satisfy Gem::Installer#install
  mfile = open("Makefile", "wb")
  mfile.puts '.PHONY: install'
  mfile.puts 'install:'
  mfile.puts "\t" + '@echo "Extensions not installed, falling back to pure Ruby version."'
  mfile.close

end

Как было предложено в этом вопросе, этот ответ также требует следующей логики для загрузки кода возврата Ruby в основной библиотеке:

lib/foo.rb(выдержка)

begin
  # Extension target, might not exist on some installations
  require 'foo/foo'
rescue LoadError
  # Pure Ruby fallback, should cover all methods that are otherwise in extension
  require 'foo/foo_pure_ruby'
end

После этого маршрута также требуется некоторая манипуляция задачами rake, так что задача rake по умолчанию не пытается скомпилировать Rubies, которые мы тестируем, и не имеет возможности скомпилировать расширения:

Rakefile (выдержки)

def can_compile_extensions
  return false if RUBY_DESCRIPTION =~ /jruby/
  return true
end 

if can_compile_extensions
  task :default => [:compile, :test]
else
  task :default => [:test]
end

Обратите внимание, что часть Rakefile не обязательно должна быть полностью общей, она просто должна охватывать известные среды, которые мы хотим локально построить и протестировать жемчужину (например, все цели Тревиса).

Я заметил одно раздражение. То есть по умолчанию вы увидите сообщение Ruby Gems Building native extensions. This could take a while... и никаких указаний на то, что компиляция расширений была пропущена. Однако, если вы вызываете установщик с помощью gem install foo --verbose, вы видите сообщения, добавленные в extconf.rb, поэтому это не так уж плохо.

Ответ 2

Вот мысль, основанная на информации от http://guides.rubygems.org/c-extensions/ и http://yorickpeterse.com/articles/hacking-extconf-rb/.

Похоже, вы можете поместить логику в extconf.rb. Например, запросите константу RUBY_DESCRIPTION и определите, находитесь ли вы в Ruby, который поддерживает собственные расширения:

$ irb
jruby-1.6.8 :001 > RUBY_DESCRIPTION
=> "jruby 1.6.8 (ruby-1.8.7-p357) (2012-09-18 1772b40) (Java HotSpot(TM) 64-Bit Server VM       
    1.6.0_51) [darwin-x86_64-java]"

Итак, вы можете попробовать что-то вроде wrap-кода в extconf.rb в условном (в extconf.rb):

unless RUBY_DESCRIPTION =~ /jruby/ do

  require 'mkmf'

  # stuff    
  create_makefile('my_extension/my_extension')

end

Очевидно, вам понадобится более сложная логика, захват параметров, переданных на "gem install" и т.д.