Automating Cocoa Deployments with Sparkle and Xcode

Posted by mhagedorn on March 7th, 2008 filed in Cocoa

For those of us who live in the rails world, fantastic tools like Capistrano have made deployment drop-dead simple. And since I do some Cocoa work I found myself wanting some of that same capistrano-esqe love in my Cocoa deployments. The Sparkle framework handles most of this for you, but in its vanilla implementation requires you to hand code some descriptor files and this seemed problematic to me. So I have cooked up an Xcode based recipe which allows me to do push button deploys and let Sparkle handle the updating part.

I got the germ of the idea for this one from Duncan Davidson’s blog post (which sadly is no longer available due to a server crash) of last fall where he too was longing for something like capistrano to automatically handle pushing up code updates. I will condense what I remember from his post.

At its heart, this approach starts by recognizing that Xcode allows you to keep project specific settings (kind of like environment variables) in a separate file called an .xcconfig file. For us this file will contain the version number of the code that we are going to deploy. In this technique this value will be manually editing. Everytime we want to do a deploy, edit the xcconfig file, then do a deployment. The deployed code will show up on the client with the version number you specified in the .xcconfig file.

Create the Xcode Config File

In your cocoa project, add a new file to the project (File -> New File) and select Xcode/Configuration Settings File. Call it whatever works for you. Open the file up in Xcode and add this single line to it (or more if you are using xcconfig for other things too :) )

WIDGET_VERSION = 0.99

(Replace WIDGET with whatever the name is of your application, its up to you, you are merely defining a variable here that you will use in your Plist ).

The next step is to make sure XCode uses the xcconfig file that we just created. To do this, select the project icon in the outline view of Xcode and hit command-I. This will bring up the project information for your project. Navigate to the build tab on this window, and at the bottom of this tab you should see “Based on:”, with a drop down to pick an xcconfig file to base this build on. Since we are going to control the version number through the xcconfig, we want to select the xcconfig file that holds our version number here.

Modify the PList file to utilize the setting from the .xcconfig file

Lets now edit Info.plist file to make use of the version number. Edit the plist so that it looks like this (I have only listed the appropriate lines, leave the lines alone)

<key>CFBundleVersion</key> <string>${WIDGET_VERSION}</string>

Save this and lets move on…

Create a new build target which does the deploy

Add a new target to your XCode project, and call it Deploy. Drag your build product under this target as well. This step will make sure that building the application will happen before deploying it (it makes it a dependency). Add a new build phase to your Deploy target, add a build phase that is of type Run Script. ” Add -> New Run Script Build Phase”. Edit this run script build phase to look like this:

if [ "Release" != "$CONFIGURATION" ]; then
    echo 'error: Package creation only works for Release builds'
    exit -1
fi

./ruby_deploy.rb $WIDGET_VERSION $CONFIGURATION_BUILD_DIR`

Create the Ruby File Which Does the Packaging

In our run script phase we just wrote, we turn around and execute a ruby script called ruby_deploy. This is a custom script that we are going to create right now. Go ahead and add a new empty file to your project, and save it as ruby_deploy.rb. Here is a suggested starting place that I use. Feel free to modify it as needed for what you are doing

#!/usr/bin/env ruby
#
#  ruby_deploy.rb
#  widget
#
#  Created by Mike Hagedorn on 12/1/07.
#  Copyright (c) 2007 Silverchair Solutions. All rights reserved.
#


require 'rss/1.0'
require 'rss/2.0'
require 'open-uri'
require 'rss'

require 'net/http'


#put your application name here
APPLICATION_NAME="widget"
#put your user name here
USERNAME="user"
#specify the appropriate server and directories here
SCP_DESTINATION="yourserver.com:the_directory_where_you_want_it_on_your_webserver/apps/#{APPLICATION_NAME}"
SCP_SOURCE="/tmp"


version = ARGV[0]
build_dir = ARGV[1]
proper_name = APPLICATION_NAME.capitalize
package = "#{proper_name}_#{version}.zip"
notes = "#{APPLICATION_NAME}_#{version}.html"
puts build_dir
`zip -r /tmp/#{package} \"#{build_dir}/PhonoscopeDialer.app\"`
url_base = "http://yourserver.com/apps/#{APPLICATION_NAME}"
dist_dir = url_base

content = ""
# raw content of rss feed will be loaded here
open("#{url_base}/appcast.xml") do |s|
    content = s.read
end
rss = RSS::Parser.parse(content, false)

package_url = "#{url_base}/#{package}"
zip_ctime = File.ctime("#{SCP_SOURCE}/#{package}")
zip_size = File.size("#{SCP_SOURCE}/#{package}")
pub_date = zip_ctime.localtime.strftime("%a, %d %b %Y %T %z")

last_item = rss.items.last

latest_version = last_item.title.split.last
if version.to_f <= latest_version.to_f
    puts "version not updated, returning"
    exit -1
end


source = "#{SCP_SOURCE}/#{package}"
dest = "#{USERNAME}@#{SCP_DESTINATION}/#{package}"
`scp \"#{source}\" \"#{dest}\"`

content_new = RSS::Maker.make("2.0") do |m|
    m.channel.title = "Your Application Log"
    m.channel.link = "#{url_base}/appcast.xml"
    m.channel.description = "Most recent changes with links to updates."
    m.channel.language = "en"
    rss.items.each do |item|
        m.items.new_item do |newitem|
            newitem.title = item.title
            newitem.link = item.link
            newitem.description = item.description  
            newitem.enclosure.url = item.enclosure.url  
            newitem.enclosure.type = item.enclosure.type
            newitem.enclosure.length = item.enclosure.length
            newitem.date = item.date
        end
    end
#add the new one
m.items.new_item do |appendedItem|
    appendedItem.title = "Your Application Version #{version}"
    appendedItem.description = "#{url_base}/#{APPLICATION_NAME}_#{version}.html"
    appendedItem.enclosure.url = "#{package_url}"
    appendedItem.enclosure.type = 'application/octet-stream'
    appendedItem.enclosure.length = "#{zip_size}"
    appendedItem.date = pub_date        
end


end

puts content_new
File.open("#{SCP_SOURCE}/appcast.xml","w") do |f|
    f.write(content_new.to_s)
end


source = "#{SCP_SOURCE}/appcast.xml"
dest = "#{USERNAME}@#{SCP_DESTINATION}/appcast.xml"
`scp \"#{source}\" \"#{dest}\"`

source = "#{build_dir}/#{notes}"

dest = "#{USERNAME}@#{SCP_DESTINATION}/#{notes}"
`scp \"#{source}\" \"#{dest}\"`

Lets step through this script. The first thing to do (and this will be custom to your application) is to set up values for a specific server on the internet where your application will be stored. The variables set up here for the location of the appcast need to match the SUFeedURL entry in your Plist file. You would have had to set that up when you integrated Sparkle. Heres an example that would match the ruby file listed above.

<key>SUFeedURL</key> <string>http://www.yourserver.com/apps/widget/appcast.xml</string>

The next thing that is done is to create the archive (zip file) of the built application. That’s why building the application is a prerequisite for the deploy step, you want to make sure you have code there to actually zip up. After you zip up the file, you store it in the /tmp directory.

Now we need to update the appcast.xml file, this is the RSS feed that your application will check to see if there is a more recent drop of the application available. The basic workflow is, go out to the internet, grab the appcast.xml feed (the xml), parse the file to find the last revision number, if the current revision is equal to the last revision, exit and do nothing. If however the revision number is greater (this should usually be the case) then go ahead and copy the zipped up code out to the server using scp, secure copy. Then you need to create an RSS item for the new current revision (the update) and append it to the collection of items in the RSS feed. Then you simply copy back the new and improved appcast.xml file out to your server. Here is a simple example of an appcast file, to get this process started you need to copy this file out to your server first (so that it can be fetched and parsed)

<?xml version="1.0" encoding="utf-8"?> 
    <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"> 
    <channel> 
        <title>Your Application Log</title> 
        <link>http://www.yourserver.com/apps/widget/appcast.xml</link> 
        <description>Most recent changes with links to updates.</description> 
        <language>en</language> 
        <item> 
            <title>Version 0.9</title> 
            <description>http://www.yourserver.com/apps/widget/widget_0.9.html</description> 
            <pubDate>Fri, 30 Nov 2007 19:20:11 +0000</pubDate> 
            <enclosure url="http://www.yourserver.com/apps/widget/widget_0.9.zip" length="1600000" type="application/octet-stream"/> 
        </item> 
    </channel> 
    </rss> 

`

Another step here that is important to the workflow is to add a release note to each drop. This is an html file that is displayed when the update is loading. The naming convention is appname_version.html. Here’s a simple example of that

<h2>Notes on version 1.02</h2>
<ul>
    <li>First Sparkle Release</li>
</ul>`

Your Deployment Workflow

Now that you have all this, how do you use it? First do all your coding, testing, etc for your next release. When you are ready to push out some changes, first create the html file listing your changes and add it to your project (so that the script can find it to copy it out to your deployment server). Use the naming convention listed above. Then update your xcconfig file with the appropriate release number. Then select the Deploy target and build. That’s it. Isn’t that cool? `

Leave a Comment

You must be logged in to post a comment.