Cobalt Edge

 
« Back to blog

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  endend

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 << "#{items['description']}]]>" }          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  endend

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 << file.strip }      # 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  endend

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.

Comments (0)

Leave a comment...