Rip & Gem Dependencies

; updated

Update: my changes have been merged into rip by Chris. Nice!

The good stuff is here, my ADD-addled friends

I’ve just spent a bit of time kicking the tyres of Rip, an alternative package manager by Chris Wanstrath.

If you’ve done any Rails development recently, and tried to keep a vendor directory of gems in order, you’ll know the pain that almost certainly inspired the Rip project.

While there are already some alternatives (bundler, dependencies), I like the elegant and lo-fi approach that Rip takes.

Environments

Rip lets you partition the dependencies you install into seperate ‘environments’. When an environment is active, the installed versions are the only ones that running code will be able to load. So, if you have one project that needs a specific version of soup, for example, you can install that version in an environment for that project.

In your default environment, you can continue to upgrade/downgrade/play about with the gems and libraries that are installed, but as long as you activate the other, known-good environment when you are working with soup, you can be sure that none of the new versions will interfere. Each environment is like a sandbox.

Unsurprisingly, it’s a lot like the cheap branching that git brings - you can play around as much as you like, but always get your environment back to working state just by typing rip use <environment>.

The elegance of Rip

I said above that I thought Rip was elegant, and that’s because these environments are just subdirectories which are added to the $RUBYLIB environment variable. This becomes part of the $LOAD_PATH for your Ruby process. Regular require statements will work as expected, and you don’t even need to require "rubygems" (which, it seems, we shouldn’t be doing anyway).

All Rip really does is try to ensure that the ‘active’ environment is injected into the $RUBYLIB; switching environments is really just moving a symlink; and the rest of the code deals with nuts and bolts of actually downloading libraries of code from various sources. Very simple, as most good things are.

All in all, when it works it’s a very pleasant development tool to use, and you could do worse than to spend a few minutes checking it out.

Gem dependencies

Rip works with git repositories, directories on the local filesystem, or over HTTP, and has rudimentary support for install rubygems too.

However, the current version (0.0.3) doesn’t install any gem dependencies. So, if I want to play about with monk, which I did want to do today, Rip is only going to download the monk gem itself, and not any of its dependencies. It’s actually going to be a bit of a pain to even find out what the dependencies are.

If I’m going really start putting Rip through its paces, I need to be able to quickly and reliably install the same libraries that I can install using normal Rubygems, including their dependencies.

Time to stop whining

So, today I rolled up my sleeves and taught Rip how to download gem dependencies too.

It wasn’t quite as easy as I hoped - it took me a bit of head-scratching to really figure out the relationship between Packages, the PackageManager, the Installer and the two GemPackages, but thanks to a bit of help from Gabriel Horner’s previous work, I have it working in a reasonably stable way.

Here’s my fork of rip installing Rails:

➜ $ rip install rails Searching gems.github.com for rails... ERROR: Could not find rails in any repository Searching gems.rubyforge.org for rails... Searching gems.github.com for rake... ERROR: Could not find rake in any repository Searching gems.rubyforge.org for rake... Searching gems.github.com for activesupport... ERROR: Could not find activesupport in any repository Searching gems.rubyforge.org for activesupport... Searching gems.github.com for activerecord... ERROR: Could not find activerecord in any repository Searching gems.rubyforge.org for activerecord... Searching gems.github.com for actionpack... ERROR: Could not find actionpack in any repository Searching gems.rubyforge.org for actionpack... Searching gems.github.com for actionmailer... ERROR: Could not find actionmailer in any repository Searching gems.rubyforge.org for actionmailer... Searching gems.github.com for activeresource... ERROR: Could not find activeresource in any repository Searching gems.rubyforge.org for activeresource... Successfully installed rake (0.8.7) Successfully installed activesupport (2.3.3) Successfully installed activerecord (2.3.3) Searching gems.github.com for rack... ERROR: Could not find rack in any repository Searching gems.rubyforge.org for rack... Successfully installed rack (1.0.0) Successfully installed actionpack (2.3.3) Successfully installed actionmailer (2.3.3) Successfully installed activeresource (2.3.3) Successfully installed rails (2.3.3) ➜ $

Next steps

I’m sure my approach could be improved. Here’s the issue on github, if you’ve got any feedback, or would like to see efforts here continue.

I’d also quite like to have Rip respect the sources (and their ordering) as defined in .gemrc, but that’s just my preference.

Now, to finally start playing with monk

(oh, btw, here’s rip installing monk; I don’t know why the dependencies gem needs wycats-thor…)

➜ $ rip install monk Searching gems.github.com for monk... ERROR: Could not find monk in any repository Searching gems.rubyforge.org for monk... Searching gems.github.com for thor... ERROR: Could not find thor in any repository Searching gems.rubyforge.org for thor... Searching gems.github.com for dependencies... ERROR: Could not find dependencies in any repository Searching gems.rubyforge.org for dependencies... Successfully installed thor (0.11.5) Searching gems.github.com for wycats-thor... Successfully installed wycats-thor (0.11.5) Successfully installed dependencies (0.0.6) Successfully installed monk (0.0.5) ➜ $

Another update, a few days later

You can see the gem dependency changes in the rip source now. It now delegates as much as it can to the gem binary, which means it will respect the gem sources you already have installed (rubyforge, github, gemcutter and anything else).

Older versions of gems now install properly too

I had a bit of fun figuring out some kinks in the dependency identification for specific versions of rubygems. Normally, you can find out the dependencies of a gem, without installing it, by running

$ gem dependency <gem> --remote

However, this doesn’t seem to work for many gems, particularly ones that aren’t the most recent. For example:

$ gem dependency rails --remote
Gem rails-2.3.3
  rake (>= 0.8.3, runtime)
  activesupport (= 2.3.3, runtime)
  activerecord (= 2.3.3, runtime)
  actionpack (= 2.3.3, runtime)
  actionmailer (= 2.3.3, runtime)
  activeresource (= 2.3.3, runtime)

But when I try requesting the dependencies for a specific version:

$ gem dependency rails --version="2.2.2" --remote
No gems found matching rails (= 2.2.2)

No dice. The simple solution we’ve adopted is to download the gem at the specific version requested, and then read the dependencies from the gem itself, using gem specification to produce a YAML version of its gemspec:

$ gem fetch rails --version="2.2.2"
Downloaded rails-2.2.2
$ gem specification rails-2.2.2.gem
--- !ruby/object:Gem::Specification
name: rails
version: !ruby/object:Gem::Version
  version: 2.2.2
platform: ruby
authors:
- David Heinemeier Hansson
autorequire:
bindir: bin
cert_chain: []

date: 2008-11-20 23:00:00 +00:00
default_executable: rails
dependencies:
- !ruby/object:Gem::Dependency
  name: rake
  type: :runtime
  version_requirement:
  version_requirements: !ruby/object:Gem::Requirement
    requirements:
    - - ">="
      - !ruby/object:Gem::Version
        version: 0.8.3
    version:
(etc)

File conflicts

One of the selling points of rip is that dependency issues are resolved when you are building the environment, and not when your application actually runs. As part of this, rip will now [warn you when one library is trying to overwrite a file that was already installed by another package][fil-conflicts]. This means that two packages can’t both try to install a file called node.rb (although they can install package_a/node.rb and package_b\node.rb, so nicely namespaced code will work just fine).

However, this means that my monkrb example doesn’t work anymore, because of the wycats-thor dependency:

➜ $ rip install monk Successfully installed thor (0.11.5) Some files from wycats-thor (0.11.5) conflict with those already installed by thor: lib/thor/actions/create_file.rb lib/thor/actions/directory.rb lib/thor/actions/empty_directory.rb lib/thor/actions/file_manipulation.rb lib/thor/actions/inject_into_file.rb lib/thor/actions.rb lib/thor/base.rb lib/thor/core_ext/hash_with_indifferent_access.rb lib/thor/core_ext/ordered_hash.rb lib/thor/error.rb lib/thor/group.rb lib/thor/invocation.rb lib/thor/parser/argument.rb lib/thor/parser/arguments.rb lib/thor/parser/option.rb lib/thor/parser/options.rb lib/thor/parser.rb lib/thor/rake_compat.rb lib/thor/runner.rb lib/thor/shell/basic.rb lib/thor/shell/color.rb lib/thor/shell.rb lib/thor/task.rb lib/thor/util.rb lib/thor.rb bin/rake2thor bin/thor rip: installation failed ➜ $

The solution here isn’t really clear. I think that the dependencies gem shouldn’t really depend on a user-specific version of thor, and there’s some kind of irony there. One way to proceed might be to add a new flag to rip install, something like rip install --no-dependencies which only installs the package in question and ignores its dependencies.

Anyway, please do kick the tires around, join the mailing list and contribute!