Sometimes you need to quickly hack a RubyGem to do your bidding. You already know it's a terrible idea. You already know you should buck up and submit a patch to the gem's maintainer. But you just can't stop yourself from tweaking. Habits die hard.
psych gem bundles up all the YAML goodness already available by default in
the MRI Ruby 1.9 series. But, currently, installing the
psych gem on Windows
isn't as nice as it could be. Psych assumes you've already installed the libyaml
development headers and libraries on your system, and it assumes you've made
yaml.dll available on your
%PATH% for runtime use.
As a Windows user, what you'd really like to do is
gem install psych and have
everything just work. But if installation was already that easy, there would be
no point in this post would there?
So let's give
psych a smarter brain that groks easier Windows installs.
However, the real point is to show how easy it is to crack open an existing
RubyGem and modify it to suit your needs. If you're a natural salesperson you'll
try to sell this as "code reuse" rather than the lurking maintenance fiasco it
Ready Dr. Frakenstein?
If you haven't done so already, download a DevKit
MSYS/MinGW toolchain and inject its goodness into your Ruby by following
If you'd like to use GCC v4.6.2 I've added a build recipe to the RubyInstaller project.
rake devkit dkver=mingw-32-4.6.2 sfx=1 and look in the
Next, you need the
libyaml development headers and libraries. They're really easy
to build from source using my libyaml-waf
build recipe or you can download a pre-built binary from my GitHub repo.
Extract the contents to
Now, fetch the
psych gem, extract its
gemspec, and unpack everything into a
work directory in preparation for modifying
C:\Users\Jon\Downloads\temp>gem fetch psych Fetching: psych-1.2.2.gem (100%) Downloaded psych-1.2.2 C:\Users\Jon\Downloads\temp>gem spec psych-1.2.2.gem --ruby > psych.gemspec C:\Users\Jon\Downloads\temp>gem unpack psych-1.2.2.gem Unpacked gem: 'C:/Users/Jon/Downloads/temp/psych-1.2.2' C:\Users\Jon\Downloads\temp>move psych.gemspec psych-1.2.2 1 file(s) moved. C:\Users\Jon\Downloads\temp>cd psych-1.2.2
Now it's time to make
psych smarter by tweaking how it's built. The idea is
to update its
extconf.rb to allow static linking to
libyaml and update its
version and build date information to minimize potential conflicts with the
First, patch the existing
diff --git a/ext/psych/extconf.rb b/ext/psych/extconf.rb index fe7795f..772471a 100644 --- a/ext/psych/extconf.rb +++ b/ext/psych/extconf.rb @@ -17,6 +17,13 @@ end asplode('yaml.h') unless find_header 'yaml.h' asplode('libyaml') unless find_library 'yaml', 'yaml_get_version' +# --enable-static option statically links libyaml to psych +# XXX only for *nix or MinGW build toolchains +if ARGV.include?('--enable-static') + $libs.gsub!('-lyaml', '-Wl,-static -lyaml -Wl,-shared') + $defs.push('-DYAML_DECLARE_STATIC') +end + create_makefile 'psych' # :startdoc:
Next, update the version and date information in both
# file: psych.gemspec Gem::Specification.new do |s| s.name = "psych" s.version = "1.2.3.alpha.1" ... s.authors = ["Aaron Patterson"] s.date = "2012-02-24" ... end # file: lib\psych.rb module Psych # The version is Psych you're using VERSION = '1.2.3.alpha.1' ... end
If all went well, we should be able to build our own little Fraken-psych and
watch it successfully install. The key is to use
--with-libyaml-dir and the new
--enable-static options when you invoke
Let's flip the switch and see see what happens.
C:\Users\Jon\Downloads\temp\psych-1.2.2>gem build psych.gemspec Successfully built RubyGem Name: psych Version: 1.2.3.alpha.1 File: psych-1.2.3.alpha.1.gem C:\Users\Jon\Downloads\temp\psych-1.2.2>gem install psych-1.2.3.alpha.1.gem -- --with-libyaml-dir=c:\devlibs\libyaml --enable-static Temporarily enhancing PATH to include DevKit... Building native extensions. This could take a while... Successfully installed psych-1.2.3.alpha.1 1 gem installed C:\Users\Jon\Downloads\temp\psych-1.2.2>gem list psych *** LOCAL GEMS *** psych (1.2.3.alpha.1)
Time for a quicktest to see how it works. I use
ripl but there's no reason
you can't use good old
irb. Note the use of
gem psych before
in order to use our updated version of
C:\Users\Jon\Downloads\temp\psych-1.2.2>ripl >> gem 'psych' => true >> require 'psych' => true >> [ Psych::LIBYAML_VERSION, Psych::VERSION ] => ["0.1.4", "1.2.3.alpha.1"] >> Psych.load "- this\n- is\n- an array\n- of strings" => ["this", "is", "an array", "of strings"]
That look's pretty good. But given that Aaron has spent time creating a large test
suite, it would be a shame to quit at the quicktest. We're going to return to
the work directory, build, copy the resulting
lib\psych, and run
psych test suite. This example assumes the DevKit is installed in
C:\Users\Jon\Downloads\temp\psych-1.2.2>cd ext\psych C:\Users\Jon\Downloads\temp\psych-1.2.2\ext\psych>\DevKit\devkitvars.bat Adding the DevKit to PATH... C:\Users\Jon\Downloads\temp\psych-1.2.2\ext\psych>ruby extconf.rb --with-libyaml-dir=c:\devlibs\libyaml --enable-static extconf.rb:7: Use RbConfig instead of obsolete and deprecated Config. checking for yaml.h... yes checking for yaml_get_version() in -lyaml... yes creating Makefile C:\Users\Jon\Downloads\temp\psych-1.2.2\ext\psych>make generating psych-i386-mingw32.def compiling emitter.c compiling parser.c compiling psych.c compiling to_ruby.c compiling yaml_tree.c linking shared-object psych.so C:\Users\Jon\Downloads\temp\psych-1.2.2\ext\psych>xcopy psych.so ..\..\lib C:psych.so 1 File(s) copied C:\Users\Jon\Downloads\temp\psych-1.2.2\ext\psych>cd ..\.. C:\Users\Jon\Downloads\temp\psych-1.2.2>for %F in (test\psych\test_*.rb) do ruby -Ilib -Itest %F ... C:\Users\Jon\Downloads\temp\psych-1.2.2>ruby -Ilib -Itest test\psych\test_yamldbm.rb ... .....F............ Finished tests in 0.811202s, 22.1893 tests/s, 57.9387 assertions/s. 1) Failure: test_key(Psych::YAMLDBMTest) [test/psych/test_yamldbm.rb:80]: <"a"> expected but was
. 18 tests, 47 assertions, 1 failures, 0 errors, 0 skips
Not perfect, but not bad. Only one of the official tests failed. Time to submit
an issue to
psych's GitHub issue tracker.
As you can see, it's straightforward to modify an existing RubyGem if need be. But while it may be easy to do, I don't recommend it for anything but experimentation. Take your hard work and wrap it up into a patch for the maintainers. You've got better things to do with your time than maintain a one-off fork and gem maintainers typically love contributions, especially those that fix problems or enhance usability.