I decided to write a command line tool with RubyMotion to visualise CGRects and NSRects from lldb, but I quickly discovered that RubyMotion does not support this out of the box.
You can build an OS X app that doesn't present a UI by by setting LSUIElement
to true
and not creating any UI, but when you copy the binary out of the bundle and try to run it, you get an error that looks something like this:
drawrect[59471:707] No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting
This is due to the NSApplicationMain
function not being able to find an Info.plist
due to the binary no longer residing inside a bundle. We need to tweak RubyMotion's compile process to skip this method.
Replacing NSApplicationMain
I tracked down the offending snippet of code within lib/motion/project/template/osx/config.rb
.
main_txt << <<EOS
int
main(int argc, char **argv)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
EOS
if ENV['ARR_CYCLES_DISABLE']
main_txt << <<EOS
setenv("ARR_CYCLES_DISABLE", "1", true);
EOS
end
main_txt << <<EOS
RubyMotionInit(argc, argv);
NSApplication *app = [NSApplication sharedApplication];
[app setDelegate:[NSClassFromString(@"#{delegate_class}") new]];
EOS
if spec_mode
main_txt << "SpecLauncher *specLauncher = [[SpecLauncher alloc] init];\n"
main_txt << "[[NSNotificationCenter defaultCenter] addObserver:specLauncher selector:@selector(appLaunched:) name:NSApplicationDidFinishLaunchingNotification object:nil];\n"
end
main_txt << <<EOS
NSApplicationMain(argc, (const char **)argv);
[pool release];
rb_exit(0);
return 0;
}
EOS
As you can see on line 139, it calls NSApplicationMain
which is the source of the error we're seeing as NSApplicationMain
tries to load the NIB as well as construct the application object. See Matt Gallager's article on NSApplicationMain for more details.
Patching RubyMotion's compile step
We could do this one of two ways:
- Create a new OS X command line template
- Monkey-patch the Config class
Given I'm kinda lazy, monkey-patching the config class was the way to go for me :)
Make a directory called lib
in the root of the RubyMotion project, and make a file named osx_cli.rb
.
We need to patch the main_cpp_file_txt
method of the Motion::Project::OSXConfig
class, so your file should look something like this:
module Motion::Project
class OSXConfig < XcodeConfig
def main_cpp_file_txt(spec_objs)
end
end
end
Then you want to paste the original method body in, and then we can start patching!
Let's remove the NSApplicationMain
call as we won't be needing that (yet).
Command line applications don't need an application delegate either, so let's delete the following lines:
NSApplication *app = [NSApplication sharedApplication];
[app setDelegate:[NSClassFromString(@"#{delegate_class}") new]];
However, we still need an entry point into our Ruby code. Let's use the pre-existing delegate_class
variable to define the entry point class.
main_txt << <<EOS
[[NSClassFromString(@"#{delegate_class}") new] main];
[pool release];
rb_exit(0);
return 0;
}
EOS
Now rather than calling NSApplicationMain
, we're now calling the main
instance method on the application delegate class (which is AppDelegate
by default).
To make RubyMotion actually pick our code up, we need to require
it in the Rakefile
.
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/osx'
require_relative 'lib/osx_cli'
begin
require 'bundler'
Bundler.require
rescue LoadError
end
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'drawrect'
end
When you run your app, it should now call AppDelegate#main
!
What if I still need a run loop?
I thought I was home scot-free at this point, however when I tried to initialize a NSWindow
in the daemon component of drawrect
, I ran into these errors:
drawrect[61361:707] _NXCreateWindowWithStyleMask: error setting window property (1000)
drawrect[61361:707] error [1000] setting colorSpace to DELL 3008WFP colorspace
drawrect[61361:707] PSsetwindowlevel, error setting window level (1000)
drawrect[61361:707] _NSSetWindowTag, error clearing window tags (1000)
drawrect[61361:707] _NSSetWindowTag, error setting window tags (1000)
drawrect[61361:707] error [1000] getting window resolution
drawrect[61361:707] Error [1000] setting resolution to 1
drawrect[61361:707] _NSShapePlainWindowWithOpaqueRect: error setting window shape (1000)
drawrect[61361:707] CGSAddSurface failed - error 1000 (windowID:4017)
drawrect[61361:707] CGSAddSurface failed - error 1000 (windowID:4017)
These errors are occuring because the app no longer has a connection to the window server since we have removed the [NSApplication sharedApplication]
line, which connects the app to the window server.
All we need to do is call NSApplication.sharedApplication
and run the main event loop:
def bootServer!
app = NSApplication.sharedApplication
app.delegate = self
NSApp.run
end
def applicationDidFinishLaunching(notification)
DrawRect.new.listen
end
And boom, everything works! drawrect
is now a self-contained command line application that has the ability to create windows and doesn't have to reside in an app bundle.
Check out the drawrect
project at https://github.com/chendo/drawrect for a fully functioning example.
Let me know if this has been useful to you!
Comments