Redistributable Rails applications
Imagine you have a large Rails application that you are going to distribute. That’s might be a new world-crashing CMS or incredibly modern Redmine fork. Every separate installation produced by a consumer requires different configs. Or maybe even some code that will adapt your product for particular needs using amazing internal API.
Clever consumer will also want to store such a “deployment” in his own git repository. And as the last point – he will definitely require a nice way to maintain ability to upgrade your product within required version branch.
How do you achieve that?
Let me share my story first. I manage two banking products: Roundbank and Smartkiosk. They are Rails applications. Every time bank wants to deploy Roundbank-based internet banking I need a way to:
- Get application core and create a nice new look that will match bank’s design using internal API.
- Extend core with the transport methods that are required to integrate with bank’s core banking platform.
- Support it.
First two steps are pretty easy. It can even be a fork on the Github. And then comes third. Release management crashes. Especially if bank has own team that’s involved. Another downside of forks is that your consumer has the whole codebase inside his project. You might not think so but… damn! So provocative! You remember he’s not supposed to change anything right?
The solution to the dependency management is wide-known – Ruby Gems. Gems have nice versioning system that will solve the issue. You have a Rails application – can it be a gem at the same time? Answer is yes.
I wrote a tiny gem called Matrioshka. It contains the set of generators that will make everything on your behalf. Following sections will describe it’s internals. You can skip it safely to the end of the article to read about gem itself.
So what exactly do we need to allow another Rails application include the whole application as a gem?
1. gemspec, init.rb
Every gem starts with a gemspec and initialization routines. You will need the following files:
init.rb. Here is what Roundbank contains (patched a bit :):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 2 3
Rails has out-of-box solution called Rails Engines. All you have to do is to extend your
lib/$application.rb a bit.
1 2 3 4 5 6 7 8
Rails Engines system was created to make Rails applications extendible by gems. But it’s abilities are underestimated. It will even run
config/environments for you. In fact it will transparently include most of your project with just the following code.
3. I18n, autoload_path, migrations
Mot of your project. Excluding some options. We need to help it a bit with a clever initializer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
This will proxy your locales, autoloadable pathes and even migrations! Note that there is popular approach to copy migrations from gems. Two words: NO WAY. Described initializer will allow you to seamlessly run migrations from both sources. They will stay ordered.
Seeds are not handled by Rails Engines too. And moreover you can’t improve your situation from within your gem. However all you need to do is to extend
db/seeds.rb of descendant project with the following line:
This is the worst part. Ruby Gems are great. However some parts of it do not hold water.
You can not use gems from git
Okay it might be a strange requirement. But did you never use it with the Bundler itself? It’s extremely comfortable and useful. Are you ready to abandon it? I am not.
You can not split gems for platforms
Roundbank can work under MRI and JRuby. And it uses slightly different set of gems for different platforms. What am I supposed to do with that? There are some workarounds that invoke proper dependencies of a particular platform from within compilation hooks – don’t even try those. They will not work with Bundler well. They will stay ignored for
:path => inclusion and even
:git => inclusion. The worst thing is that new Ruby Gems 2.0 are ought to be released. And still no progress.
The best option I was able to come up with is to copy host project
Gemfile to every descendant project. Put it to, say,
Gemfile.roundbank and then require:
6. Transparent initialization
During initial startup Rails relies on
Foo::Application constant heavily. You might
grep you code for that – it’s everywhere. Rack setup, Environments, Initializers, …. But now that we are trying to run it in a very special way – it will fail.
Foo::Application will not exist in inherited context. Instead of that we are supposed to configure the descendant.
And here comes magic. During class initialization at
config/application.rb your application instance is storead at
Rails.application property. The final step required to make your application gem-compatible is to replace
Rails.application.class everywhere. Here is the total list of locations you should check:
1 2 3 4 5 6 7 8 9
This replacement makes the code application-indepent. No matter what application runs it – it always uses proper instances.
As soon as these 6 steps are done – you can pack your new gem and use it from any other Rails application. At the same time host application will remain runable from itself also.
But why do all that steps manually if Matrioshka can do that for you?
I tested this approach at Roundbank and fell in love. To extend it to other products and automate the 5th step I created the Matrioshka gem. It will do everything for you with it’s mighty generators.
Host Application (Gem)
Inject the following to your host application Gemfile:
Run Matrioshka install generator
It will generate all the required additions and patches. For a typical application they will just work. However you probably should edit
$application.gemspec to set proper meta information for your future gem.
Client Application (Consumer)
As soon as your gem is ready to rumble we can procceed to the consumer. Let’s make it work within a new rails application:
Add your application gem to the new Gemfile:
bundle install and then
Ta-dam. You are done here. Time to party hard!