Boško Ivanišević
Developer, Theoretical Physicist
Ruby, JavaScript, Elixir, C++ and few more

Exefy ’Em All

Introduction

Recently Luis Lavena started thread (Idea: executable stubs to replace batch) on RubyInstaller mailing list. In short, the idea is to use command line applications - executable stubs - instead of batch files in RubyInstaller Ruby versions. Using command line applications instead of batch files has several benefits. First one, maybe not so important, is to avoid annoying

Terminate batch job (Y/N)?

question when execution is interrupted with Ctrl-C key combination. The second is to get meaningful list of processes in the system. With batch files we can only see bunch of ruby.exe processes in the list. This gives us possibility to define firewall rules for these applications which will not be applied globally for all Ruby scripts. Finally, installing Ruby applications as services, with the help of some service wrapper, is usually easier if we use executable file. These are reasons why new gem-exefy gem was made.

Gem-exefy Internals

Gem-exefy mimics behavior of batch files installed by RubyGems and to see how it works we must know how existing batch files work. First step is to install gem that has executable value defined in gem specification. Example of such gem is Bundler. After installing it on Windows, RubyGems will create bundle.bat file in <path_to_ruby_installation>/bin folder. Content of that file is:

@ECHO OFF
IF NOT "%~f0" == "~f0" GOTO :WinNT
@"ruby.exe" "c:/path/to/ruby/installation/bin/bundle" %1 %2 %3 %4 %5 %6 %7 %8 %9
GOTO :EOF
:WinNT
@"ruby.exe" "%~dpn0" %*

Without digging too much into all details we can see that batch file starts ruby.exe passing it full path to Ruby script (bundle, in this case) using all arguments passed to batch file as arguments of Ruby script. So all we have to do is to make application which will be able to execute Ruby script and will accept arguments passed in the command line. Sounds familiar, isn’t it? Exactly! We already have ruby.exe and in Ruby code we can find almost everything we need. Here is a slightly simplified version of Ruby’s main.c file:

#include "ruby.h"

#ifdef HAVE_LOCALE_H
#include <locale.h>
#endif

int
main(int argc, char **argv)
{
#ifdef HAVE_LOCALE_H
  setlocale(LC_CTYPE, "");
#endif

  ruby_sysinit(&argc, &argv);
  {
    RUBY_INIT_STACK;
    ruby_init();
    return ruby_run_node(ruby_options(argc, argv));
  }
}

If we build application from this source we will get the same application as ruby.exe. This means if we want to execute some Ruby script we will have to pass path to it as a first argument with optional arguments following it. But our goal is not to invoke application with the path to Ruby script. Instead we want to invoke predefined script. In order to achieve that, we obviously have to alter the list of arguments (argv) and to insert path to target Ruby script. But there is a catch (I spent almost whole day to figure it out). We must change the list of arguments after the call

ruby_sysinit(&argc, &argv);

Ruby performs system dependent arguments initialization in the above method and the list of arguments will be reverted back if we change it before this initialization. Of course there are some additional details that we must take care of, but you can figure them out directly from “the source”. After we change the list of arguments, Ruby will execute script we passed it and that’s all the magic gem-exefy does.

Let’s Exefy

We are now ready to exefy existing and new gems on our RubyInstaller version. gem-exefy is made as RubyGems plugin. After installing it with:

gem install gem-exefy

new gem command will be available - exefy. This command is used for replacing batch files for single or all installed gems. Replacing batch files with executable stubs for single gem is performed by passing name of the targeted gem to the exefy command.

gem exefy bundler

Exefying all installed gems is simple - just pass `–all` to `exefy` command

gem exefy --all

If you are not satisfied and still want to use batch files - don’t worry. You can always revert old batch files for single or all gems with `–revert` argument

gem exefy bundler --revert
gem exefy --all --revert

After gem-exefy is installed it will, by default, install executable stubs instead of batch files for all gems installed after it.

It is important to mention that gem-exefy will not replace batch files for commands installed with RubyInstaller (irb, rake…) and are part of Ruby core. Will support for converting these batch files be implemented is yet to be seen.

Acknowledgements

I want to thanks @luislavena, @azolo and @jonforums for helping me out making gem-exefy.