Detecting ruby binary compilation layout in runtime - ruby

Quite often Ruby installed with RVM (the 'most popular tool to install Ruby') produces a binary not capable of SSL operations (like connecting via HTTPS). It's a known issue with a known workaround.
If you're trying to use SSL with such Ruby binary it results in exception LoadError: cannot load such file -- openssl.
Question: is there any way I can tell which libs Ruby binary was built against and thus detect if it is capable of SSL or not at runtime? Other than trying to actually use SSL and catching the exception?

Unless you want to do the same work that require does, I think the easiest is to try to require openssl and rescue the LoadError in case it's not present on the system to detect its availability.
It seems to be an idiom commonly used.
If you wanted something that detects afterwards if it was loaded or not would
ruby -ropenssl -e 'p defined? OpenSSL' # => "constant"
vs.
ruby -e 'p defined? OpenSSL' # => nil
help you?
That is, checking whether the return value of defined?(OpenSSL) is non-nil?
We commonly use this in tests to exclude tests at runtime where features that are not available to all versions of OpenSSL are used, and to exclude tests completely (if defined? OpenSSL) if OpenSSL is not present as an extension.

Related

How do I get rrdtool from homebrew to work with ruby on macOS

In our Rails application we do require 'RRD' at some point, but that results in a cannot load such file -- RRD. So obviously I used homebrew to install rrdtool, but the error remains.
The docs at https://oss.oetiker.ch/rrdtool/prog/rrdruby.en.html provide two options:
Either:
$: << '/path/to/rrdtool/lib/ruby/1.8/i386-linux'
require "RRD"
In my /opt/homebrew/Cellar/rrdtool/1.8.0/lib directory there's no mention of ruby, which is because of the --disable-ruby-site-install flag in the formula, because when I skip that flag I do actually get something: /opt/homebrew/Cellar/rrdtool/1.8.0/lib/ruby/2.6.0/universal-darwin21. However replacing the path/to string with this path still gives the error.
Or:
If you use the --ruby-site-install configure option you can drop the $: line since the RRDtool module will be found automatically.
Which is a little confusing (and probably outdated) because here it seems that ruby site install is disabled by default and you have to enable it proactively, whereas in the formula it's actually actively disabled.
Either way: both options didn't do the trick for me and if there's a solution without homebrew that's also fine.
For good measure: I'm on macOS Monterey
TL;DR
For the most part, I'd say that using a non-standard gem without a Ruby version manager is your main issue. There are instructions on the rrdruby site for installing it, but they don't follow typical conventions, so your mileage will vary.
Some Practical Suggestions
The require keyword is for gems, not binaries. You need to have an rrdtool-related gem installed, available to your Ruby instance (usually through a Bundler Gemfile or gemspec, or via the RUBYOPTS environment variable or your in-process Ruby $LOAD_PATH), and then require the correct name of the gem in your code. For example, using the older rrd-ffi gem:
# use sudo if you're installing it to the system,
# but I would strongly recommend a ruby version
# manager instead
gem install rrd-ffi
# in your Ruby class/module file
require "rrd"
For the gem you seem to be using, you have to compile the gem first to make it usable, and then ensure it's available in your Ruby $LOAD_PATH (or other gem lookup mechanism) before trying to require it. The error message you're seeing is basically telling you that a gem with that name is not available as called within any of the standard lookup locations.
Again, I'd suggest reading the build documentation for your gem, and then seeing if you can install it as part of a Bundler bundle, RVM gemset, or other non-system approach if you can. Otherwise, follow the directions for the rrdruby tool, which is not available as a standard Rubygems.org gem, in order to make it available before trying to require it.
Beware of Outdated or Non-Standard Gems
Most of the RRD gems I found were quite old; most were 7-8 years old or older, so their compatibility with current Rubies is potentially suspect. The gem-builder you're using is newer, but doesn't seem to be designed as a standard gem, so you need to build it and install it in a suitable lookup path before it can be required. Installing gems as system gems is almost always a bad idea, so I'd strongly recommend building it from source and using a ruby version manager rather than following the rrdtool author's atypical suggestions. YMMV.

how can I know which Ruby executable was used in ruby script?

there are two Ruby environments on a system, normal ruby and Chef embedded ruby. I want to know, in a ruby script, which ruby executable is used to invoke the script itself. How can get that?
Recommended Solutions
Use the poorly-documented RbConfig module, if available:
RbConfig.ruby
#=> "/Users/foo/.rubies/ruby-2.7.0/bin/ruby"
Alternatively, you can use the easier-to-find Gem module from the standard library to do the same thing:
Gem.ruby
#=> "/Users/foo/.rubies/ruby-2.7.0/bin/ruby"
Other Approaches
The RbConfig and Gem modules are your best bet, but there may be times when you need to get at the version or path information another way. Here are some different approaches.
Get the Version
You can return the version of the executing Ruby as a String with:
RUBY_VERSION
#=> "2.7.0"
Get the Path
Ruby is usually installed to bin/ruby in the RUBY_ROOT. You can return the expected path to the running Ruby binary (and verify it actually exists, if necessary) as follows:
ENV["RUBY_ROOT"] + "/bin/ruby"
#=> "/Users/foo/.rubies/ruby-2.7.0/bin/ruby"
File.exist? ENV["RUBY_ROOT"] + "/bin/ruby"
#=> true
Alternatively, you can use Kernel#` to find the first Ruby in your PATH as follows:
`which ruby`.chomp
=> "/Users/foo/.rubies/ruby-2.7.0/bin/ruby"
There are certainly edge cases where either approach can be misleading, though. For example, Ruby may have been built in a non-standard way, or you may have invoked Ruby with a fully-qualified path rather than calling the first binary in PATH. That makes "roll your own" lookups less reliable, but if your environment is missing the RbConfig or Gem modules for some reason, this might be a reasonable alternative for you.

ruby - determine version of core libraries offline

For non-core libraries, gem list | grep rails will show the version (e.g. rails (5.1.4)).
For core libraries, such as openssl, without looking up the version of ruby then finding out on the internet, is there a way to locally determine what version?
OpenSSL.methods does not include an in-code way, there are no comments in the file (~/.rbenv/versions/2.4.2/lib/ruby/2.4.0/openssl.rb for me), the folder is not a git repository so I can't look at commit hashes... the only option surely can't be to checkout versions systematically and do a file by file comparison with the source code I have until I get an exact match?
A workaround solution, so not going to accept it:
Open interactive ruby documentation with ri -i
Type OpenSSL or whatever looking for, and hit enter... it tells you about it. Scroll down and you might find something like this:
Constants:
OPENSSL_FIPS: [not documented]
OPENSSL_LIBRARY_VERSION:
Version of OpenSSL the ruby OpenSSL extension is running with
OPENSSL_VERSION:
Version of OpenSSL the ruby OpenSSL extension was built with
OPENSSL_VERSION_NUMBER:
Version number of OpenSSL the ruby OpenSSL extension was built with (base
16)
VERSION:
OpenSSL ruby extension version
Thus in a ruby console you can do this:
irb> require 'openssl'
=> true
irb> OpenSSL::OPENSSL_VERSION
=> "OpenSSL 1.0.2l 25 May 2017"
So the generalised solution, is read the interactive docs, looking for a conveniently named constant, then access said constant from console.

Native extensions fallback to pure Ruby if not supported on gem install

I am developing a gem, which is currently pure Ruby, but I have also been developing a faster C variant for one of the features. The feature is usable, but sometimes slow, in pure Ruby. The slowness would only impact some of the potential users (depends which features they need, and how they use them), so it makes sense to have the gem available with graceful fallback to Ruby-only functions if it cannot compile on a target system.
I would like to maintain the Ruby and C variants of the feature in a single gem, and provide the best (i.e. fastest) experience from the gem on installation. That would allow me to support the widest set of potential users from a single project of mine. It would also allow other people's dependent gems and projects to use the best available dependency on a target system, as opposed to a lowest-common-denominator version for compatibility.
I would expect the require to fallback at runtime to appear in the main lib/foo.rb file simply like this:
begin
require 'foo/foo_extended'
rescue LoadError
require 'foo/ext_bits_as_pure_ruby'
end
However, I don't know how to get the gem installation to check (or try and fail) for native extension support so that the gem installs correctly whether or not it can build 'foo_extended'. When I researched how to do this, I mainly found discussions from a few years back e.g. http://permalink.gmane.org/gmane.comp.lang.ruby.gems.devel/1479 and http://rubyforge.org/pipermail/rubygems-developers/2007-November/003220.html that imply Ruby gems do not really support this feature. Nothing recent though, so I am hoping someone on SO has some more up-to-date knowledge?
My ideal solution would be a way to detect, prior to attempting a build of the extension, that the target Ruby did not support (or perhaps simply not want, at the project level) C native extensions. But also, a try/catch mechanism would be OK if not too dirty.
Is this possible, if so how? Or is the advice to have two gem variants published (e.g. foo and foo_ruby), that I am finding when I search, still current best practice?
This is my best result attempting to answer my own question to date. It appears to work for JRuby (tested in Travis and on my local installation under RVM), which was my main goal. However, I would be very interested in confirmations of it working in other environments, and for any input on how to make it more generic and/or robust:
The gem installation code expects a Makefile as output from extconf.rb, but has no opinion on what that should contain. Therefore extconf.rb can decide to create a do nothing Makefile, instead of calling create_makefile from mkmf. In practice that might look like this:
ext/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
As suggested in the question, this answer also requires the following logic to load the Ruby fallback code in the main library:
lib/foo.rb (excerpt)
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
Following this route also requires some juggling of rake tasks, so that the default rake task doesn't attempt to compile on Rubies that we're testing on that don't have capability to compile extensions:
Rakefile (excerpts)
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
Note the Rakefile part doesn't have to be completely generic, it just has to cover known environments we want to locally build and test the gem on (e.g. all the Travis targets).
I have noticed one annoyance. That is by default you will see Ruby Gems' message Building native extensions. This could take a while..., and no indication that the extension compilation was skipped. However, if you invoke the installer with gem install foo --verbose you do see the messages added to extconf.rb, so it's not too bad.
https://stackoverflow.com/posts/50886432/edit
I tried the other answers and could not get them to build on recent Rubies.
This worked for me:
Use mkmf#have_* methods in extconf.rb to check for everything you need. Then call #create_makefile, no matter what.
Use the preprocessor constants generated by #have_* to skip things in your C file.
Check which methods/modules are defined in Ruby.
If you want to support JRuby et al, you'll need a more complex release setup.
A simple example where the whole C extension is skipped if something is missing:
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.
If you want to release the gem for JRuby, you need to adapt the gemspec before packaging. This will allow you to build and release multiple versions of the gem. The simplest solution I can think of:
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'
Then run $ rake package && gem push pkg/my_gem-0.1.0 && gem push pkg/my_gem-0.1.0-java to release a new version.
If you just want to run on JRuby, not distribute the gem for it, this will suffice (it will not work for releasing the gem, though, as it is evaluated before packaging):
my_gem.gemspec
if RUBY_PLATFORM !~ /java/i
s.extensions = %w[ext/my_gem/extconf.rb]
end
This approach has two advantages:
create_makefile should work in every environment
a compile task can remain prepended to other tasks (except on JRuby)
Here is a thought, based on info from http://guides.rubygems.org/c-extensions/ and http://yorickpeterse.com/articles/hacking-extconf-rb/.
Looks like you can put the logic in extconf.rb. For example, query the RUBY_DESCRIPTION constant and determine if you are in a Ruby that supports native extensions:
$ 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]"
So you could try something like wrap the code in extconf.rb in a conditional (in extconf.rb):
unless RUBY_DESCRIPTION =~ /jruby/ do
require 'mkmf'
# stuff
create_makefile('my_extension/my_extension')
end
Obviously, you will want more sophisticated logic, grabbing parameters passed on "gem install", etc.

how do you start ruby 1.9 without rubygems

I want my app to not be able to use any installed gems. Is there a ruby 1.9 startup parameter or way of doing this programmatically?
ruby --disable-gems
is the MRI (1.9) commandline parameter. "It prevents the addition of gem installation directories to the default load path". (The Ruby Programming Language, p. 391)
Edit 25-10-2012: Ruby core had the same idea as #rogerdpack in the comments and added the more verbose ruby --help parameter. Ruby revision!
Looking at the rubygems configuration file, I would attempt to hack out gempath or gemhome to see if you can override (instead of just append to) defaults.
If, for example, setting gempath to be empty, or to point to /dev/null, prevents using system gems, then that would be the way to go.
The main advantage to this, as I see it, is that your anti-rubygems config file can be passed to ruby 1.9 as a startup parameter (so not coded in), well documented, and checked into your repository.
All of this is, of course, disregarding that rubygems is part of ruby 1.9's standard library - so ruby may choke and die if it can't have access to its gems, depending on how much of ruby's base install requires gem functionality. YMMV.

Resources