Hacker News new | ask | show | jobs
by h2s 4598 days ago
Couldn't agree more. I made the mistake of using Ruby to build a CLI application too, and distribution is a massive hassle. At the moment I'm still at the awkward stage of distribution via Rubygems which the article advises strongly against, and I'd add "Gem startup time cripples your performance" to the article's point about Rubygems being difficult for non-Rubyists.
3 comments

The gem stat/open overhead really needs to be fixed. I have an application where rubygems causes about 35000 attempts at identifying the require'd files due to its horrific meddling with $LOAD_PATH. Whoever thought that was a good idea really did not think things true.

EDIT: Strike that - I just realised one of my apps gets 117,000 ENOENTs from trying to handle require's during startup....

EDIT: I keep wanting to write something to cache the paths, but haven't had time. I'd be perfectly happy to be forced to regenerate cache on first run after any gem update. The 117,000 ENOENT's above comes from ending up with a $LOAD_PATH of about 100 entries, where the worst case causes almost every one of those directories to be checked for both foo.rb and foo.so. Something like 99%+ of startup time of most of my Ruby code is overhead added by rubygems way of handling require.

EDIT: Actually, a lot of this might be down to bundler rather than rubygems in some cases.

EDIT: This is not a robust solution, but this little ugly helper combined with wrapping only the two require statements shown, reduced the number of failed stat() calls for my app from 117,000 to 104,700 on startup...

    $orig_loadpath = $LOAD_PATH.dup
    require 'bundler/setup'
    $full_loadpath = $LOAD_PATH.dup

    def with_gems(*gems)
      filtered = []
      gems.each do |gem|
        filtered.concat($full_loadpath.grep(/#{gem}/))
      end
      $LOAD_PATH.clear
      $LOAD_PATH.concat($orig_loadpath)
      $LOAD_PATH.concat(filtered)
      yield
      $LOAD_PATH.clear
      $LOAD_PATH.concat($full_loadpath) 
   end

    with_gems('require_relative') { require 'require_relative' if RUBY_VERSION =~ /1\.8/ }
    with_gems('amalgalite','arrayfields','fastercsv') { require 'amalgalite' }
EDIT: Wrapped a handful more require statements with "with_gems". Down to 76,000 failed stat()'s... I should have done this before.

EDIT: Yikes. 32,000...

So why don't you distribute your Ruby app without using RubyGems?

Look, I've never been able to understand the people who think RubyGems is a good distribution mechanism for end users either. But switching to another language altogether seems to throwing out the baby with the bathwater. You can just create a Debian package or something that depends on Ruby. That's exactly what we do with Phusion Passenger.

Have gem dependencies? Vendor them. Not that hard.

But if you have gem dependencies that contain native extensions that aren't distributed by the OS... well then switching to Go starts to make sense.

That's assuming everyone's using Ubuntu (or some other Debian-based OS)...

He actually did mention that in the post, though -- packaging Ruby into the installer -- and mentioned how difficult it is. I've never tried it myself, but I imagine it's pretty tough if, for example, you're distributing to Windows as well (although who uses CLI apps in Windows anymore...?)

Lots of us.

Many configurations are done via command line tools actually, more so since PowerShell exists.

agreed. i personally write CLI apps in ocaml for the ease of distributing a single, small executable, but d, go and recently nimrod are all good choices too.