-
21 May
MotionBundler: Good Old Fashioned Requirements for RubyMotion
(This is a guest post from Paul Engel, creator and maintainer of MotionBundler)
When I started writing my first RubyMotion application, I almost immediately wanted to use a Ruby gem to accomplish a certain goal. After setting up my Gemfile, running bundle and rake in the Terminal, I soon discovered that it wasn’t possible to just require any random gem I wanted to: it had to be aware of it being required in a RubyMotion app.
Suddenly, my next open source project became a fact. I wanted to create a gem which allowed you to require any Ruby gem in a RubyMotion application. My first attempt was the LockOMotion project. It gets the job done but it wasn’t test-driven enough and the code wasn’t well structured. Enter MotionBundler, a complete rewrite with a 100% test-coverage.
I am a big fan of using and writing libraries and gems that are as unobtrusive as possible. I did not want to force the developer to follow special conventions (e.g. using another method than
require
to load up code). And MotionBundler does just that.Setup and Usage
MotionBundler is intended to be easy in installation and usage. You need to setup your Gemfile by separating RubyMotion-aware Ruby gems from the ones that are not. Put the RubyMotion-unaware gems in the
:motion
Bundler group, as shown here.source "http://rubygems.org" # RubyMotion aware gems gem "motion-bundler" # RubyMotion unaware gems group :motion do gem "slot_machine" end
Then, simply addMotionBundler.setup
at the end of your Rakefile.[...] require "motion/project" # Require and prepare Bundler require "bundler" Bundler.require Motion::Project::App.setup do |app| # Use `rake config' to see complete project settings. app.name = "SampleApp" end # Track and specify files and their mutual dependencies within the :motion # Bundler group MotionBundler.setup
Finally, you can just run the
bundle
command then the defaultrake
task to build and run the application. And that’s all about it!Requiring non-Gem Sources
Just like a regular Ruby project, you are now able to require source files. It can either be a relative path on the file system or a Ruby standard library source file. As an example, let’s require the cgi Ruby standard library file and use the
CGI.escape_html
method in a RubyMotion controller.require "cgi" class AppController < UIViewController def viewDidLoad puts CGI.escape_html('foo "bar" ') end end
Looks familiar, right?
How Does MotionBundler Work?
MotionBundler traces
require
,require_relative
,load
andautoload
method calls in your code when invokingMotionBundler.setup
. All four methods eventually are delegated to therequire
method which MotionBundler hooks into. MotionBundler callsMotionBundler::Require::Tracer::Log#register
which traces the calling Ruby source and registers the source file being loaded.When
MotionBundler.setup
invokesBundler.require :motion
, all required files and their mutual dependencies are registered. Aside from the Ruby gems defined in the:motion
Bundler group, MotionBundler also usesRipper::SexpBuilder
to scan for require statements (like motion-require does) in the Ruby sources defined in./app/**/*.rb
so it can trace requirements usingMotionBundler::Require::Tracer::Log
.Console Warnings
When I was writing LockOMotion it was very difficult to debug certain errors. I have been able to override certain core methods to print warnings which contain useful debug information.
These are only printed in the iOS simulator console. When running from the device, MotionBundler does not interfere when an error gets raised. As an example, let’s check out a warning printed in the console when dealing with a runtime
require
statement.[...] Warning Called `require "base64"` Add within setup block: app.require "base64" 2013-05-21 13:45:26.851 SampleApp[17300:c07] app_controller.rb:48:in `viewDidLoad': uninitialized constant AppController::Base64 (NameError) from app_delegate.rb:5:in `application:didFinishLaunchingWithOptions:' 2013-05-21 13:45:26.855 SampleApp[17300:c07] *** Terminating app due to uncaught exception 'NameError', reason: 'app_controller.rb:48:in `viewDidLoad': uninitialized constant AppController::Base64 (NameError) from app_delegate.rb:5:in `application:didFinishLaunchingWithOptions:' [...]
Here, the base64 file is missing from the build system. You can fix that problem by adding the following code in the Rakefile.
MotionBundler.setup do |app| app.require "base64" end
RubyMotion Runtime Limitations
Unfortunately, you still cannot just require every file that works within a regular Ruby environment. You cannot require C extensions and you cannot evaluate Ruby code passed in a String (e.g. the
eval
method). This is why MotionBundler cannot ensure that you can require every Ruby gem out there. They have to be RubyMotion friendly, for example like SlotMachine.But as a last resort, MotionBundler offers you to possibility to mock source requirements by loading drop-in replacements written in pure Ruby.
Mocking Sources
Let’s say you want to use a Ruby gem which requires the stringio extension. Because stringio is a Ruby C extension, and that RubyMotion doesn’t support Ruby C extensions, that Ruby gem will not load up in RubyMotion. However, as mentioned earlier, MotionBundler offers a possibility to bypass this problem by mocking the library.
Instead of requiring the stringio.bundle file, MotionBundler is able to mock it with a pure Ruby implementation of it, taken from MacRuby. At the moment, MotionBundler also mocks strscan, zlib and httparty (only its basic features).
Aside from mocks being defined within the MotionBundler gem, you can also define your own mocks within your RubyMotion application. Just add a directory called mocks within the root directory of the application and put the mock sources in it. The relative path of the mock source within that directory ensures a certain Ruby file being mocked during compile time.
Let’s say the root directory of your RubyMotion application is ~/Sources/sample_app. If you want to mock
require "yaml"
, create a file at ~/Sources/sample_app/mocks/yaml.rb containing the mock code. If you want to mockrequire "net/http"
, create a file at ~/Sources/sample_app/mocks/net/http.rb.You aren’t supposed to mock entire Ruby gems of course, that would be crazy. But you would rather mock fundamental Ruby standard library sources (like stringio.bundle) so you don’t have to dismiss a certain Ruby gem for use in your RubyMotion app on forehand.
Please Try MotionBundler!
MotionBundler is available on Github. The repository provides a sample application. You can clone the repository, navigate to the sample application directory, run
bundle
followed byrake
. Please check out MotionBundler! Pull requests, remarks or requests are very welcome! You can also contact me on Twitter :-)Finally, I would like to say thanks to Laurent, Nick Quaranto and Dave Lee for giving me some feedback and suggestions.
You can discuss this post on the RubyMotion google group.
Paul Engel is a software developer living in beautiful Amsterdam, Netherlands.