Cobalt Edge

 
Filed under

Cocoa

 

Creating Sparkle Appcast via Rake Tasks

I have a RubyCocoa application that self-updates using Sparkle. To do so, you need to create an "appcast" file which contains the version and download information for your application, as well as creating the zip file that holds your app. Then, you of course have to upload this to the server and location that you have specified in the SUFeedURL key value in your Info.plist file of your app. For general instructions on using Sparkle and setting it up, see their Basic Instructions page.

My Rake tasks do not create the zip file. I may enhance it to do this at some point, but so far I haven't needed to, and have had cases where I need to create it myself for various reasons. What the tasks do is to build an appcast.xml file from a YAML file that contains all the necessary information. Note that the name of my app is "Linker", so you'll see that in various spots. The tasks do rely on a simple directory structure:


  • Your app root directory


    • Rakefile

    • appcast


      • version_info.yml

      • build


        • Your app zip files go here (e.g. Linker_0.8.zip, Linker_0.9.zip, etc.)

        • Rake task will create the linker_appcast.xml file here




So, you have a spot you drop your zip files into, and this same dir is where the Rake tasks create the appcast file. The version_info.yml file is where you put the info needed to generate the appcast. It looks like this:


linker-04:
title: Linker 0.4
filename: Linker_0.4.zip
description: Added Sparkle updating mechanism.

linker-05:
title: Linker 0.5
filename: Linker_0.5.zip
description: Added help (see Help menu). Added bookmarklet support/custom URL protocol handling. See the new help for information on how to use the bookmarklet.

Note that you can put HTML into the "description" field, and my Rake task will deal with and preserve that.

Finally, I have two Rake tasks, one for building the appcast, and the other for uploading it and the latest zip file to the server. These each are simply one liners that call a parallel Ruby method within the Rakefile:


namespace :appcast do
desc "Create/update the appcast file"
task :build do
make_appcast
end

desc "Upload the appcast file to the server"
task :upload do
upload_appcast
end
end

The two methods rely on you defining a couple of variables in your Rakefile, adjust these as desired:


APPCAST_SERVER = 'your_appcast_server.com'
APPCAST_URL = "http://#{APPCAST_SERVER}"
APPCAST_FILENAME = 'linker_appcast.xml'

Here are the methods, first the one that builds the appcast, which you'll need to modify for your app:


def make_appcast
begin
versions = YAML.load_file("appcast/version_info.yml")
rescue Exception => e
raise StandardError, "appcast/version_info.yml could not be loaded: #{e.message}"
end

appcast = File.open("appcast/build/#{APPCAST_FILENAME}", 'w')

xml = Builder::XmlMarkup.new(:target => appcast, :indent => 2)

xml.instruct!
xml.rss('xmlns:atom' => "http://www.w3.org/2005/Atom",
'xmlns:sparkle' => "http://www.andymatuschak.org/xml-namespaces/sparkle",
:version => "2.0") do
xml.channel do
xml.title('BWA Linker')
xml.link(APPCAST_URL)
xml.description('Linker app updates')
xml.language('en')
xml.pubDate(Time.now.rfc822)
xml.lastBuildDate(Time.now.rfc822)
xml.atom(:link, :href => "#{APPCAST_URL}/#{APPCAST_FILENAME}",
:rel => "self", :type => "application/rss+xml")

versions.each do |version|
guid = version.first
items = version[1]
file = "appcast/build/#{items['filename']}"

xml.item do
xml.title(items['title'])
xml.description { xml xml.pubDate(File.mtime(file))
xml.enclosure(:url => "#{APPCAST_URL}/#{items['filename']}",
:length => "#{File.size(file)}", :type => "application/zip")
xml.guid(guid, :isPermaLink => "false")
end
end
end
end
end


Looking through that above, you'll want to modify the title and description at least. Now on to the uploader method:

def upload_appcast
remote_dir = "/var/www/apps/bwa/shared/public/updaters/"

Net::SSH.start( APPCAST_SERVER, 'deploy' ) do |session|
cwd = Dir.pwd
Dir.chdir('appcast/build')

shell = session.shell.sync

begin
out = shell.cd remote_dir
raise "Failed to change to proper remote directory." unless out.status == 0

out = shell.ls("-1")
raise "Failed to get directory listing." unless out.status == 0

files = Array.new
out.stdout.each { |file| files
# Look through the list of files and see what we need to upload, as
# compared to what we have locally - but always upload the appcast itself
local_files = Dir.glob('*')
files.delete(APPCAST_FILENAME) # we always upload this
local_files.each do |local_file|
unless files.include?(local_file)
print "Uploading: #{local_file}... "
`scp #{local_file} deploy@#{APPCAST_SERVER}:#{remote_dir}`
puts $?.exitstatus == 0 ? "done." : "FAILED!"
end
end
rescue => e
puts "Failed: #{e.message}"
ensure
Dir.chdir(cwd)
shell.exit
end
end
end


You will of course want to modify the remote_dir, and the login credentials towards the bottom (where it does the scp command). This also relies on you having your SSH keys set up, so you don't have to enter a password when it does the scp.

You could further generalize this obviously, but this is what I have, it works fine, and I haven't needed to extract anything out further. Posting here as per a request, and hopefully it saves someone else a few minutes.

Loading mentions Retweet
Filed under  //   Cocoa   Ruby   RubyCocoa   Sparkle  

Comments [0]

RubyCocoa Rocks

My infatuation with RubyCocoa continues. I've been working on a small app for the Building Web Apps folks. I'd originally been tasked with a feature that was to be done 100% within the web app. However, some of our requirements changed, and the workflow was not efficient enough. So, essentially what we moved forward on was a desktop Mac application that could interface with NetNewsWire, as well as the Building Web Apps site. The desktop app gives us a faster way to add data into the system - rapidly processing a ton of content and injecting what is desired into the web app, yet without getting slowed down by a web app interface. AJAX or Flex, or some other web UI tech wouldn't make it any faster in this particular situation.

Thus, I pursued building a Cocoa app, but this time using RubyCocoa. I've written Objective-C apps before, and spend the bulk of my time in Ruby, but this was my first opportunity to use RubyCocoa. The combination, much like JRuby, Jython and other hybrid systems, gives you "the best of both worlds." There are cons of course (slower, a few Cocoa things you can't do, debugging is harder, etc.), but for the most part, it's really nice.

For me, the infatuation stems from letting me use the aspects of each system that I am either more efficient with, or that are easier for a particular piece of functionality, all yielding a faster, and better end result. I can iterate on the app more quickly, and get a solution to BWA faster. And, one step further in the case of RubyCocoa: having the native OS integration abilities at hand.

What's been great is the ability to use Ruby's more effective (for me at least) string processing, XML processing, and networking features; create a native Mac application - using any cool Cocoa/native features; and support for AppleScript, which was critical for this particular application. This latter feature ruled out using something like Adobe Air.

This evening I setup the second use of AppleScript/Apple Events in this app: registering a custom URL protocol for the app. Applications like Pukka and Mailplane do this. What for? Well, in this case, it allows me to create a web browser bookmarklet, that can send data from the browser to our app. It also means that it works in basically any browser on the Mac (as opposed to AppleScript not working for Firefox). Further, it allows a simple "push a button" in the browser to send the data over to the app, as opposed to having to switch to the app, pick a menu item to pull the data, or horror of horrors, copy-paste.

How do you do this? This article is short and covers how to do it in Objective-C. It's just as easy in RubyCocoa: You need to add an entry into your Info.plist file to specify the name of your URL protocol as described in the article. Then, you need to register your app as a handler for that protocol:


NSAppleEventManager.sharedAppleEventManager.
setEventHandler_andSelector_forEventClass_andEventID_(
self, :getUrl_withReplyEvent, fourcharcode('GURL'), fourcharcode('GURL'))

The fourcharcode method is my way of translating four character codes for use in Ruby. I discussed this in more detail in my last post about RubyCocoa, but here's the actual method for your pleasure:


def fourcharcode(character_code)
character_code.unpack('N').first
end

Ok, so now that you've registered your app to handle its custom URL protocol, you will get an Apple Event sent to you with the URL whenever one is opened. This is handled (as per the parameter in the registration function above), by the getUrl_withReplyEvent method:


def getUrl_withReplyEvent(event, reply)
url = event.paramDescriptorForKeyword(fourcharcode('----')).stringValue
# url now contains the complete URL as a string
# do your processing of the URL/content...
end

That's it. Pretty cool eh? Handling events from NetNewsWire is almost identical (register for them, write a handler function).

And, one more great thing I could integrate: Sparkle. Sparkle is a superb Cocoa library that does automatic application updates. It checks the web for a newer version of your app, downloads it, and installs it. Integrating it is simple, and in fact, depending on your needs, you don't have to write a single line of code. The only code I wrote for it was actually a Rake task to build the appcast and upload it to the server. Slick.

All this could only be done as a native Mac app, which means Cocoa. But, as said above, doing it with RubyCocoa gives me access to all these abilities, yet, I can do all the more heavy string and XML processing I need to do using Ruby, which is much more effective for me. Also, the web services calls and code is a lot easier for me to do in Ruby than Cocoa.

Furthermore, this is plain fun! Unlike some, I feel desktop apps still have a place, but love webapps at the same time. With RubyCocoa I can build super cool Mac apps, but do so in a language I'm happier using, yet have the power of Cocoa available to me. For me, some of the best "applications" these days are such hybrids: a web app that does your primary data storage, and gives you access to the app from "anywhere" (i.e. anywhere you can get to a browser and net connection), but a desktop app to use most of the time for faster interaction, potentially better integration on your desktop system, and so on. It's the same reason I use Mailplane (desktop app for Gmail), or PackRat (desktop app for Backpack). I suspect it's the same reason we're seeing other solutions like Adobe Air, or Google Gears. Technology is so cool, isn't it?!

Loading mentions Retweet
Filed under  //   Adobe Air   Cocoa   Ruby   RubyCocoa  

Comments [0]

Building My First RubyCocoa App - Some Notes

Today I started work on my first app built using RubyCocoa, which is now a first class citizen in MacOS X 10.5/Leopard. I had read the docs and tutorial found here. I proceeded, and ran into a few bumps along the way, so here are some notes maybe someone else will find useful...

The code for the example app RSSPhotoViewer, is not quite the same as that shown in the tutorial. Specifically:


  • The tutorial says you need to put "ib_action :method_name" after your methods that are Actions. The example code does not do that, and I found I didn't need to do it either.

  • The example code does not require osx/cocoa or include OSX, yet I had to do this in my Ruby source code in order for it to recognize the class names properly (or at least so I didn't have to prefix them with OSX).

I couldn't get Interface Builder to recognize my Ruby window controller class - i.e. it didn't show it's outlets and actions. I tried a variety of things here, but basically I finally had to go to the command line and run "rake" and let it do a command line build. I have no idea what that did differently, as I can't see any new files it generated, etc., but that resolved it - now IB can see all my outlets and actions.

My app wouldn't run, and I got a strange error in Xcode saying "The debugger is still running" etc. It appears that if your app crashes this will be the case. And, in this case, Console is your friend. Open up Console and you should see messages that will help you assess what's gone wrong.

And now for the win... dealing with Apple Events. My app wants to receive a particular apple event from NetNewsWire. NNW documents this protocol nicely, however, when registering as a handler for the event (using NSAppleEventManager.sharedAppleEventManager.setEventHandler_andSelector_forEventClass_andEventID_), you need to pass in the class and event ID that are not standard ones (they're defined by NNW/external blog protocol). Well, in Cocoa code, these are just a four character string, but as I found, a string that an unsigned long via string packing. So, it was a question of how I get these ID's in via Ruby. Luckily this turned out to be rather simple, as you can use String#unpack. And, in this case, you pass unpack "N" as the format, which is an unsigned long packed in network byte order.

Lastly, debugging. As mentioned above, Console and such are your friend. I haven't tried any shenanigans with ruby-debug or such from the command line, so that might work. But, Xcode can't debug into the Ruby code in your project (it can into the code in main.m just fine though). So, if you have weird crashes and such, check Console. Also, use NSLog, or pop alerts or what not. If someone knows a better way please do tell.

Regardless, I'm quite excited by RubyCocoa and have another couple apps that I may do with it. This app was a small one, and of course is not done in just a couple hours, but it's going to be a nice addition to the tool belt.

Loading mentions Retweet
Filed under  //   Cocoa   Mac   Ruby   RubyCocoa  

Comments [0]

Cocoa Screen Saver Prefs and Bindings (or Not)

In my latest work on my Visionary Saver screen saver, I had tried switching all my preferences to use Cocoa Bindings, to make it super easy to manage the prefs. After doing this, and having it appear to work, I realized that it does not.

The problem is that screen savers are supposed to use the ScreenSaverDefaults class to manage their preferences. This is a special Defaults class that namespaces a screen savers defaults/preferences within the defaults system, given that a screen saver is a bundle, and works within System Preferences (as opposed to being its own application). The reason it doesn't work with bindings is that you can't tell the Bindings system about ScreenSaverDefaults (to my knowledge), in the same way as you can bind to the Shared Defaults Controller. ScreenSaverDefaults requires a module name, and so on. If there's a workaround, I'd love to hear it.

I thus had to go back to manually getting and setting the preferences for Visionary. This did simplify one thing, which is the preferences settings for an NSPopUpButton, where the content values come from an array, yet the selection and setting should go to preferences. Personally, NSPopUpButtons, for simple use, are a real pain. I'm not an Interface Builder expert, but it's odd that you can set up to 3 values into an NSPopUpButton in the UI, as generic text, but if you want more, you have to setup the whole NSArrayController and its content array, and so on, then bind that to the popup, etc, etc. It's not awful, but the documentation is pretty weak in terms of a straight forward use of something like this. I suspect many other folks don't have complicated data models behind the values for some of their popup buttons, and a cleaner way to do all this would be nice.

Anyway, it's all good now, or well, it's all fixed up, and there's a new version of Visionary Saver out.

Loading mentions Retweet
Filed under  //   Apple   Cocoa   Mac   screensaver   Visionary  

Comments [0]