I recently decided that because Olearia had reached 1.0 (finally) that I should implement some sort of self update aka Sparkle to make my end users life easier.
Little did I realise that this would take me on a quest that would cover most of 2 days researching and testing to get it just right.
For starters I have to thank Allan Craig for his excellent extension
to Mark Liyanage’s initial setup of this automation.
Based on both of these I took it to the next level IMHO 😉
This version has these new features over Allan Craigs
- Makes an ‘item’ tag including a description that can be embedded directly into the appcast xml file
- If the addition to the appcast file has been previously generated the script will only update the ‘enclosure’ tag in the file leaving any changes that have been made to the description alone.
- Removed the need for the css and version html files due to the description being embedded in the appcast file.
- Added the usage of the CFBundleShortVersionString due to the fact that I’m weird and like my subversion build numbers in my about box. This is easily rectified if you don’t use this feature.
- Fixed the generation of the signing string for 10.6 (Snow Leopard). This will need to be changed if you are still using Leopard.
- The script is now multi-project aware and will look in a common place for a config file named with the name of the current project. This means that the script can be copied from project to project on your system without any changes once it has been setup.
The following is an almost direct quote from Allan Craigs blog with changes where necessary.
INTRO:
IMPORTANT: You will need to read Marc’s article and make a secure note of the private key first before moving on. I would also recommend making the ‘Deployment’ Target as well.
IMPORTANT: This Script has been heavily modified from its original state and updated for Snow Leopard 10.6, if using this under 10.5 you will need
to change the multi-line ‘security’ back to its original form as per Marc or Allan’s posts.
INSTRUCTIONS:
Project Config YAML file
A YAML file is an editable text file and makes it easy to set up our
configuration information. It also enables us not to have to change the main
script once we have it the way we want it.
Create a ‘{Your_Project_Name}.yaml’ file and place it in the folder that contains
all of your other projects config files.
Personally I like to keep my things relatively organized so my path to my configs is ‘/Users/username/Code/Appcasts/configs/’
NOTE: see the configs_folder_path Variable below
include the following making the necessary changes.
—–
download_base_url: ‘http://www.your_website.com/app_folder/’
appcast_basefolder: ‘/users/user_name/desktop/app_name/’
appcast_xml_name: ‘add_to_appcast.xml’
keychain_privkey_name: ‘Sparkle Private Key’
—–
IMPORTANT: If you change the variable names here you also
need to change them in the script.
VARIABLE EXPLANATION
download_base_url:
Your website url where you will place your updated project
appcast_basefolder:
The base file is created for you and a project folder inside that with
the name of your project and version number.
example
- ProjectName
- ProjectName 1.0
- add_to_appcast.xml (contains the 'item' info)
- ProjectName 1.0.zip
- ProjectName 1.1
- add_to_appcast.xml (contains the 'item' info)
- ProjectName 1.1.zip
The following file is created for you if they do not already exist.
appcast_xml_name:
Your archived project file is also copied to the project folder
AppName {version number}.zip
appcast_xml_name:
This file holds the results of the script. You will copy this into your appcast file.
Name to your liking.
NOTE: This file only has placeholders for description information you will need to edit this to complete your feature list before adding it to your appcast file.
keychain_privkey_name:
You should understand this after reading Marc’s article.
Name to your liking.
Once your project’s config file is created and placed in your configs folder,
edit the @configs_folder_path below in the initialize method to point to its correct location.
Now if you have not done this already, create a new aggregated Target named “Deployment”
in your XCode project and drag your current target into it.
Add this script as a ‘Run Script’ build phase on the “Deployment” Target, Marc has some nice pics showing how to do this.
Set the bash to /usr/bin/ruby and you are finished!
Now you can use the “Deployment” target with a release build mode to generate the initial files and sign your package.
NOTE: if you are a developer that does not use the CFBundleShortVersionString as I do in this case it is simply a matter of removing the ‘@short_version’ from the ‘instantiate_project_variables ‘ method then doing a search for ‘@short_version’ and replace with ‘@version’
Please bear with me as this was my first experience with Ruby which made for an interesting learning curve.
Leave a comments if you would like the script emailed or have any other queries, errata or issues.
Hope this helps Someone out there.
#!/usr/bin/env ruby -w
#################################################################################
# #
# appcast_builder.rb #
# #
# author: Craig Williams #
# created: 2009-01-09 #
# #
# modified and updated by: Kieren Eaton #
# Date: 2010-01-14 #
# #
#################################################################################
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
#################################################################################
=begin
INTRO:
In his article, Marc uses a bash script to automate the process of signing your
Sparkle enabled app and explains how to put your private and public keys in your
keychain. I wrote a Ruby version that extends this functionality to
include a few more options…
IMPORTANT: You will need to read Marc's article first before moving forward.
IMPORTANT: This Script has been heavily modified from its original state
and updated for Snow Leopard 10.6, if using this under 10.5 you will need
to change the
Here is a quick list of updated features:
1. automatic selection of correct config file for a given project
2. Creates project release folder if it does not exist
3. Creates sub-folders based on version number
4. Creates xml file containing the basic '' info generated by the script
5. updates the pubDate and enclosure Tags if the basic item info already exists
6. Copies the newly created archive to the project folder
The project release folder is not the 'Release' folder Xcode creates.
This is a folder that contains this and future Sparkle release builds.
See 'appcast_basefolder' below.
INSTRUCTIONS:
Config YAML file
A YAML file is easily editable and is a good place for us to set up our
configuration information. It also enables us not to have to change the
script once we have it the way we want it.
Create a 'Your_Project_Name.yaml' (where Your_Project_Name is the name of the project) file
and place it in the folder that contains all of your other projects config files.
NOTE: see the configs_folder_path Variable below
include the following making the necessary changes.
---
download_base_url: 'http://www.your_website.com/app_folder/'
appcast_basefolder: '/users/user_name/desktop/app_name/'
appcast_xml_name: 'appcast.xml'
keychain_privkey_name: 'Sparkle Private Key'
IMPORTANT: If you change the variable names here you also
need to change them in the script.
VARIABLE EXPLANATION
download_base_url:
Your website url where you will place your updated project
appcast_basefolder:
The base file is created for you and a project folder inside that with
the name of your project and version number.
eg - ProjectName
- ProjectName 1.0
- appcast.xml (contains the '' info)
- 1.0.html
- rnotes.css
- ProjectName 1.0.zip
- ProjectName 1.1
- appcast.xml (contains the '' info)
- 1.1.html
- rnotes.css
- ProjectName 1.1.zip
The following files are created for you if they do not already exist.
appcast_xml_name:
Your archived project file is also copied to the project folder
AppName {version number}.zip
appcast_xml_name:
This file holds the results of the script. What is between the '' tags that
you will copy into your complete appcast.xml file.
Name to your liking.
keychain_privkey_name:
You should understand this after reading Marc's article.
Name to your liking.
Once your config.yaml file is created and placed in your projects 'Release' folder,
edit the "" below in the initialize method to point to its correct location.
Now create a new aggregated Target named "Deployment" in your XCode project and drag your
current target into it. Add this script as a 'Run Script' build phase on the "Deployment" Target.
Set the bash to /usr/bin/ruby and you are finished!
Now you can use the "Deployment" target with a release build mode to generate the initial files and sign your package.
If you have questions, ideas or bug reports please post them at:
This script and methodology is based heavily on Allen Craig's blog posts:
Subversion 1.6, XCode 3.1.2 & Versions
=end
class AppCast
require 'yaml'
require 'tmpdir'
require 'fileutils'
MESSAGE_HEADER = 'APPCAST BUILD MESSAGE'
def initialize
@signature = ''
# ---------------------
# edit this path to point to your folder that contains all your appcast application configs
@configs_folder_path = '/Code/Appcasts/configs/'
# ---------------------
require_release_build
instantiate_project_variables
load_config_file
instantiate_appcast_variables
end
def main_worker_bee
create_appcast_folder_and_files
remove_old_zip_create_new_zip
file_stats
create_key
create_appcast_xml
copy_archive_to_appcast_path
end
# Only works for Release builds
# Exits upon failure
def require_release_build
if ENV["BUILD_STYLE"] != 'Release'
log_message("Deployment target requires 'Release' build style")
exit
end
end
# Exits if no config.yaml file found.
def load_config_file
project_name = ENV['PROJECT_NAME']
config_file_path = "#{@configs_folder_path}#{project_name}.yaml"
if !File.exists?(config_file_path)
log_message("No '#{project_name}.yaml' file found in configs directory.")
exit
end
@config = YAML.load_file(config_file_path)
end
def instantiate_project_variables
@proj_dir = ENV['BUILT_PRODUCTS_DIR']
@proj_name = ENV['PROJECT_NAME']
@version = `defaults read "#{@proj_dir}/#{@proj_name}.app/Contents/Info" CFBundleVersion`
@version = @version.gsub(/\D+/,"")
@short_version = `defaults read "#{@proj_dir}/#{@proj_name}.app/Contents/Info" CFBundleShortVersionString`
@archive_filename = "#{@proj_name} #{@short_version.chomp}.zip"
@archive_path = "#{@proj_dir}/#{@archive_filename}"
end
def instantiate_appcast_variables
@appcast_xml_name = @config['appcast_xml_name'].chomp
@appcast_basefolder = @config['appcast_basefolder'].chomp
@appcast_proj_folder = "#{@config['appcast_basefolder']}/#{@proj_name}_#{@short_version}".chomp
@appcast_xml_path = "#{@appcast_proj_folder}/#{@appcast_xml_name}"
@download_base_url = @config['download_base_url']
@keychain_privkey_name = @config['keychain_privkey_name']
@download_url = "#{@download_base_url}#{@archive_filename}"
end
def remove_old_zip_create_new_zip
Dir.chdir(@proj_dir)
`rm -f #{@proj_name}*.zip`
`zip -qr "#{@archive_filename}" "#{@proj_name}.app"`
end
def copy_archive_to_appcast_path
begin
FileUtils.cp(@archive_path, @appcast_proj_folder)
rescue
log_message("There was an error copying the zip file to appcast folder\nError: #{$!}")
end
end
def file_stats
@size = File.size(@archive_filename)
@pubdate = `date +"%a, %d %b %G %T %z"`
end
def create_key
priv_key_path = "#{Dir.tmpdir}/priv_key.pem"
intermed_file = "#{Dir.tmpdir}/intermed_data"
temp = `security find-generic-password -g -s "#{@keychain_privkey_name}" 2>&1 1>/dev/null \
| perl -pe '($_) = /"(.+)"/'`
File.open(intermed_file, 'w+') { |f| f.puts temp.split("\\012") }
key = `perl -MXML::LibXML -e 'print XML::LibXML->new()->parse_file("#{intermed_file}")->findvalue(q(//string[preceding-sibling::key[1] = "NOTE"]))'`
log_message(key)
if key == ''
log_message("Unable to load signing private key with name '#{@keychain_privkey_name}' from keychain\nFor file #{@archive_filename}")
exit
end
File.open(priv_key_path, 'w+') { |f| f.puts key }
@signature = `openssl dgst -sha1 -binary < '#{@archive_path}' \
| openssl dgst -dss1 -sign '#{priv_key_path}' \
| openssl enc -base64`
`rm -fP #{priv_key_path}`
`rm -fP #{intermed_file}`
log_message(@signature)
if @signature == ''
log_message("Unable to sign file #{@archive_filename}")
exit
end
end
def create_appcast_xml
# if the file exists it may have already been edited
# so dont overwrite it
if !File.exists?(@appcast_xml_path)
appcast_xml =
"<item>
<title>Version #{@short_version.chomp}</title>
<description><![CDATA[
<h2>New in #{@short_version.chomp}</h2>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
]]></description>
<pubDate>#{@pubdate.chomp}</pubDate>
<enclosure
url=\"#{@download_url.chomp}\"
sparkle:version=\"#{@version.chomp}\"
sparkle:shortVersionString=\"#{@short_version.chomp}\"
type=\"application/octet-stream\"
length=\"#{@size}\"
sparkle:dsaSignature=\"#{@signature.chomp}\"
/>
</item>"
File.open(@appcast_xml_path, 'w') { |f| f.puts appcast_xml }
else
update_appcast_xml
end
end
def update_appcast_xml
new_enclosure = "<pubDate>#{@pubdate.chomp}</pubDate>
<enclosure
url=\"#{@download_url.chomp}\"
sparkle:version=\"#{@version.chomp}\"
sparkle:shortVersionString=\"#{@short_version.chomp}\"
type=\"application/octet-stream\"
length=\"#{@size}\"
sparkle:dsaSignature=\"#{@signature.chomp}\"
/>
</item>"
File.open(@appcast_xml_path, 'r+') do |f|
lines = f.readlines
count = 0
# remove all lines after and including the pubDate tag
lines.each do |it|
if it =~ /<pubDate>/
break
end
count += 1
end
lines.slice!(count..-1)
f.pos = 0
f.print lines
# add the new enclosure tags and close item tag
f.print new_enclosure
f.truncate(f.pos)
end
end
# Creates the appcast folder if it does not exist
# or is accidently moved or deleted
def create_appcast_folder_and_files
base_folder = @appcast_basefolder
project_folder = @appcast_proj_folder
Dir.mkdir(base_folder) if !File.exists?(base_folder)
Dir.mkdir(project_folder) if !File.exists?(project_folder)
end
def log_message(msg)
puts "\n\n----------------------------------------------"
puts MESSAGE_HEADER
puts msg
puts "----------------------------------------------\n\n"
end
end
if __FILE__ == $0
newAppcast = AppCast.new
newAppcast.main_worker_bee
newAppcast.log_message("It appears all went well with the build script!")
end