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 << 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
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.
5 comments:
This is badass, man. Thanks!
I might bundle up the code + tasks + initial generator into a rubygem. Thoughts on a gem name?
going with sparkle_tools; will ping back with github url when done
Dr Nic, I'm honored to have your comments! As you maybe can tell, I haven't worked on this in quite some time. I wound up having to remove Sparkle from the app I was working on as it was causing problems and we weren't able to find out why/fix that. Too bad, Sparkle rocks. I no longer work on that app though, so haven't even been at this in general. Do indeed post the GitHub URL here though, so folks can see that (and I'll add an update to the blog entry to mention it as well).
@Chris - http://github.com/drnic/choctop
Its working and documented, yay :)
I'm just preparing a nifty webpage with a sexy picture of a Choc-Top icecream :)
Post a Comment