我正在开发一个gem,它目前是纯Ruby,但我也一直在为其中一个功能开发一个更快的C变种。该功能在纯Ruby中可用,但有时很慢。缓慢只会影响一些潜在用户(取决于他们需要哪些功能,以及他们如何使用它们),因此如果无法在目标系统上进行编译,那么将gem提供给具有Ruby功能的优雅回退是有意义的。
我想在一个gem中维护该功能的Ruby和C变体,并在安装时提供gem的最佳(即最快)体验。这将使我能够从我的一个项目中支持最广泛的潜在用户。它还允许其他人的依赖宝石和项目使用目标系统上最佳的可用依赖性,而不是兼容性最低的通用分母版本。
我希望require
在运行时回退显示在主lib/foo.rb
文件中,就像这样:
begin
require 'foo/foo_extended'
rescue LoadError
require 'foo/ext_bits_as_pure_ruby'
end
但是,我不知道如何让gem安装检查(或尝试和失败)本机扩展支持,以便gem安装正确,无论它是否可以构建'foo_extended'。当我研究如何做到这一点时,我主要发现几年前的讨论,例如{3}}和http://permalink.gmane.org/gmane.comp.lang.ruby.gems.devel/1479暗示Ruby宝石并不真正支持此功能。最近没有什么,所以我希望SO上的某些人有更多最新的知识?
我的理想解决方案是在尝试构建扩展之前检测目标Ruby不支持(或者可能根本不希望在项目级别)C本机扩展的方法。但是,如果不是太脏,尝试/捕获机制也可以。
这是可能的,如果是这样的话怎么样?或者建议发布两个宝石变体(例如foo
和foo_ruby
),我在搜索时发现,仍然是当前的最佳实践?
答案 0 :(得分:1)
以下是基于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]"
所以你可以尝试在extconf.rb中将代码包装在条件中(在extconf.rb中):
unless RUBY_DESCRIPTION =~ /jruby/ do
require 'mkmf'
# stuff
create_makefile('my_extension/my_extension')
end
显然,你需要更复杂的逻辑,抓住传递“gem install”等参数。
答案 1 :(得分:1)
这是我迄今为止试图回答我自己的问题的最佳结果。它似乎适用于JRuby(在Travis和我在RVM下的本地安装中测试),这是我的主要目标。但是,我对其在其他环境中工作的确认以及如何使其更通用和/或更健壮的任何输入非常感兴趣:
gem安装代码需要Makefile
作为extconf.rb
的输出,但对于应该包含的内容没有意见。因此,extconf.rb
可以决定创建不执行任何操作 Makefile
,而不是从create_makefile
调用mkmf
。在实践中可能看起来像这样:
<强>转/富/ 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
部分不必是完全通用的,它只需要涵盖我们想要在本地构建和测试gem的已知环境(例如所有Travis目标)。
我注意到一个烦恼。默认情况下,您将看到Ruby Gems的消息Building native extensions. This could take a while...
,并且没有迹象表明已跳过扩展编译。但是,如果您使用gem install foo --verbose
调用安装程序,则会看到添加到extconf.rb
的消息,因此它不会太糟糕。
答案 2 :(得分:1)
https://stackoverflow.com/posts/50886432/edit
我尝试了其他答案,无法让他们建立在最近的红宝石上。
这对我有用:
extconf.rb
中的mkmf#have_*
methods来检查您需要的所有内容。然后拨打#create_makefile
,无论如何。#have_*
生成的预处理器常量来跳过C文件中的内容。一个简单的例子,如果缺少某些内容,将跳过整个C扩展名:
1。
ext/my_gem/extconf.rb
require 'mkmf'
have_struct_member('struct foo', 'bar')
create_makefile('my_gem/my_gem')
2。
ext/my_gem/my_gem.c
#ifndef HAVE_STRUCT_FOO_BAR
// C ext cant be compiled, ignore because it's optional
void Init_my_gem() {}
#else
#include "ruby.h"
void Init_my_gem() {
VALUE mod;
mod = rb_define_module("MyGemExt");
// attach methods to module
}
#endif
3。
lib/my_gem.rb
class MyGem
begin
require 'my_gem/my_gem'
include MyGemExt
rescue LoadError, NameError
warn 'Running MyGem without C extension, using slower Ruby fallback'
include MyGem::RubyFallback
end
end
4。 如果要为JRuby发布gem,则需要在打包之前调整gemspec 。这将允许您构建和发布多个版本的gem。我能想到的最简单的解决方案:
Rakefile
require 'rubygems/package_task'
namespace :java do
java_gemspec = eval File.read('./my_gem.gemspec')
java_gemspec.platform = 'java'
java_gemspec.extensions = [] # override to remove C extension
Gem::PackageTask.new(java_gemspec) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
pkg.package_dir = 'pkg'
end
end
task package: 'java:gem'
然后运行$ rake package && gem push pkg/my_gem-0.1.0 && gem push pkg/my_gem-0.1.0-java
以发布新版本。
如果您只想在JRuby上运行,而不是为它分发gem,这就足够了(但是,它不适用于释放gem,因为它在打包之前进行了评估):
my_gem.gemspec
if RUBY_PLATFORM !~ /java/i
s.extensions = %w[ext/my_gem/extconf.rb]
end
这种方法有两个好处:
create_makefile
应该适用于所有环境compile
任务可以保留在其他任务之前(JRuby除外)