Handle gems and their versions for one file scripts - ruby

I have a directory full of scripts mostly written in ruby. Most of them have only require calls, so after some time it is not always clear which gem provides the file or if gem changes — the script can become incompatible.
I can add gem calls specifying versions, but if combined with require calls will look excessive for small scripts.
Is there something like inline bundler without lock file, so only specifying gem versions, handling version resolution, automatic requiring and everything inline in ruby script?

If it 'looks excessive' then perhaps it is? This is purely subjective.
Using gem 'gem_name', 'version.number.s' is the way to do so, as I imagine you are doing from your words.
Once you get to the point where you need more than one file, because the organization demands it, do so. Don't fight something that is naturally occurring without a reason.
For example, for a test that only runs when called directly in this "small script" for whatever measurement of small qualifies as that limitation...
def hello(name_ = 'World', name: name_)
"Hello #{name}"
end
if __FILE__ == $PROGRAM_NAME
gem 'minitest', '5.7.0'
require 'minitest/autorun'
describe hello do
it 'must return "Hello World:"' do
assert_equal hello, 'Hello World!'
end
end
end
Having the gem specified there as is, does not require a Gemfile.lock (it isn't a gem, and doesn't include or have a Gemfile) only requires that I have that gem available / installed.
It will complain if that gem is not available.

Related

Ruby Uninitialized constant error when gem is run outside of bundle exec

I have a simple gem that creates an MD5 from a string.
module SimpleMD5
require 'digest/md5'
def self.md5_string(string)
Digest::MD5.hexdigest(string)
end
end
Running bundle exec bin/console and calling the method works fine
require 'simple_md5'
SimpleMD5.md5_string('test')
=> 098f6bcd4621d373cade4e832627b4f6
Once the gem is built using rake build and rake install using the IRB console and the same example above I get an error.
NameError: uninitialized constant SimpleMD5::Digest
Am I missing a step when the gem is built?
You're missing the module definition, so use this pattern:
require 'digest/md5'
module SimpleMD5
def md5_string(string)
Digest::MD5.hexdigest(string)
end
extend self
end
The SimpleMD5 name is not generated automatically, you must declare it somewhere.
Here extend self means you can mix-in the module with include SimpleMD5 as well as just use it straight-up as you do in your example.
Don't forget two things: In Ruby return is implicit, there's no need to use that unless you're exiting your function early, and MD5 is a pretty terrible hashing algorithm for 2016. Unless you're using it for backwards compatibility, use something better like SHA256 or SHA512.

Is there like an established way of distributing non-ruby scripts with rubygems?

If I add a bash script as an executable in a gem, installing the gem causes the executable to get wrapped in something like:
#!/usr/bin/env ruby_executable_hooks
#
# This file was generated by RubyGems.
#
# The application 'my_bash_script' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
if ARGV.first
str = ARGV.first
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
version = $1
ARGV.shift
end
end
gem 'my_bash_script', version
load Gem.bin_path('my_bash_script', 'my_bash_script', version)
This makes it dysfunctional because the ruby interpreter obviously doesn't speak bash.
What's a good way of dealing with this? Can I turn the wrapping off (preferable, as bash starts about 12 times faster than the YARV ruby interpreter), or do I need to wrap my bash in ruby in order for the generated load wrapper to be happy?
I found the following answer in one of the rubygem repo's ticket discussions.
I don't know of a good way to do this that works across platforms and makes sense.
Rubygems is for ruby. If a file is in your gem's bindir and listed as a gem executable, it should be ruby. If your gem has "special needs" then it should do it out of band, like create a Rakefile that does your special steps for you and list the Rakefile as an extension in the spec. Or just use a post-install message telling people to install with --no-wrapper.
Also I found the following comment in rubygems specification documentation.
#
# For example, the rake gem has rake as an executable. You don’t specify the
# full path (as in bin/rake); all application-style files are expected to be
- # found in bindir.
+ # found in bindir. These files must be executable ruby files. Files that
+ # use bash or other interpreters will not work.
#
Hope this information helps.

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.

Ruby: Custom gem demands a 'require' for module therein

I've built a custom Ruby gem. Call it MyGem, then file lib/innermodule.rb contains:
module MyGem
module InnerModule
def self.foo(); puts "Hello world!"; end
end
end
To reference this from another gem that's in development I have to do:
require 'mygem'
require 'innermodule'
Is this normal behaviour, or is there a problem with the gemspec for MyGem?
I don't know if this is necessarily a problem with your gemspec since you usually just specify what files to include in a gem. Gemspecs don't really have anything to do with the way a gem gets required into another app.
It sounds like a problem with the way your gem is built/packaged specifically with regards to naming and file path conventions.
There are some common conventions that are usually followed for building gems and what I referenced above
http://guides.rubygems.org/patterns/ has a good overview.
Basically, you usually want to create a single file (usually the name of your gem) that sits in the "lib" directory. In this case, "lib/mygem.rb" would have individual requires for the internal dependencies of the gem.
#lib/mygem.rb
require 'innermodule'
Then to include the gem (as well as the inner module) in any other app, you could just do
require 'mygem'

How to reference a class in a gem?

Really novice question ...
The many apps in my project have a lot of shared code in a gem. I'm trying to add a routine in that gem that will be shared by rake tasks in several apps.
So in the gem lib/utilities directory I have cleanup.rb:
module Utilities
class Cleanup
def self.perform
puts "Performing cleanup"
end
end
end
Then in the app, in lib/tasks, I have cleanup.rake:
require "utilities/cleanup"
namespace :mynamespace do
task :do_cleanup => :environment do
Utilities::Cleanup.perform
end
end
All I get from rake mynamespace:do_cleanup is "uninitialized constant Utilities::Cleanup".
I've tried several variations of the above. My gems are managed with bundler, but "bundle exec rake ..." gives the same result. I tried using gem "utilities/cleanup" instead of require.
(I swear, I tear my hair out trying to figure out how to communicate between modules and classes more than anything else in this language! It's always "which code am I referencing here?" and "why can't it find that class I wrote?")
You may need to write ::Utilities::Cleanup.perform to explicitly use the top level namespace.

Resources