Sparkle Appcast Automation in XCode update

16 01 2010

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:
    http://allancraig.net/blog/?p=65

=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[&quot;BUILD_STYLE&quot;] != 'Release'
        log_message(&quot;Deployment target requires 'Release' build style&quot;)
        exit
      end
  end

  # Exits if no config.yaml file found.
  def load_config_file
  	project_name = ENV['PROJECT_NAME']
  	config_file_path = &quot;#{@configs_folder_path}#{project_name}.yaml&quot;
	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 &quot;#{@proj_dir}/#{@proj_name}.app/Contents/Info&quot; CFBundleVersion`
    @version                = @version.gsub(/\D+/,&quot;&quot;)
    @short_version          = `defaults read &quot;#{@proj_dir}/#{@proj_name}.app/Contents/Info&quot; CFBundleShortVersionString`
    @archive_filename       = &quot;#{@proj_name} #{@short_version.chomp}.zip&quot;
    @archive_path           = &quot;#{@proj_dir}/#{@archive_filename}&quot;
  end

  def instantiate_appcast_variables
    @appcast_xml_name       = @config['appcast_xml_name'].chomp
    @appcast_basefolder     = @config['appcast_basefolder'].chomp
    @appcast_proj_folder    = &quot;#{@config['appcast_basefolder']}/#{@proj_name}_#{@short_version}&quot;.chomp
    @appcast_xml_path       = &quot;#{@appcast_proj_folder}/#{@appcast_xml_name}&quot;
    @download_base_url      = @config['download_base_url']
    @keychain_privkey_name  = @config['keychain_privkey_name']
    @download_url           = &quot;#{@download_base_url}#{@archive_filename}&quot;
  end

  def remove_old_zip_create_new_zip
    Dir.chdir(@proj_dir)
    `rm -f #{@proj_name}*.zip`
    `zip -qr &quot;#{@archive_filename}&quot; &quot;#{@proj_name}.app&quot;`
  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 = &quot;#{Dir.tmpdir}/priv_key.pem&quot;
	intermed_file = &quot;#{Dir.tmpdir}/intermed_data&quot;
    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(&quot;\\012&quot;) }

	 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(&quot;Unable to load signing private key with name '#{@keychain_privkey_name}' from keychain\nFor file #{@archive_filename}&quot;)
      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(&quot;Unable to sign file #{@archive_filename}&quot;)
      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 =
    &quot;&lt;item&gt;
	&lt;title&gt;Version #{@short_version.chomp}&lt;/title&gt;
	 <description><![CDATA[
		&lt;h2&gt;New in #{@short_version.chomp}&lt;/h2&gt;
		&lt;ul&gt;
                &lt;li&gt;Item 1&lt;/li&gt;
                &lt;li&gt;Item 2&lt;/li&gt;
         &lt;/ul&gt;

		]]&gt;&lt;/description&gt;
     &lt;pubDate&gt;#{@pubdate.chomp}&lt;/pubDate&gt;
	&lt;enclosure
		url=\&quot;#{@download_url.chomp}\&quot;
		sparkle:version=\&quot;#{@version.chomp}\&quot;
		sparkle:shortVersionString=\&quot;#{@short_version.chomp}\&quot;
		type=\&quot;application/octet-stream\&quot;
		length=\&quot;#{@size}\&quot;
		sparkle:dsaSignature=\&quot;#{@signature.chomp}\&quot;
	/&gt;
    &lt;/item&gt;&quot;

    File.open(@appcast_xml_path, 'w') { |f| f.puts appcast_xml }
	else
		update_appcast_xml
    end
  end

  def update_appcast_xml
  	new_enclosure = &quot;&lt;pubDate&gt;#{@pubdate.chomp}&lt;/pubDate&gt;
	&lt;enclosure
		url=\&quot;#{@download_url.chomp}\&quot;
		sparkle:version=\&quot;#{@version.chomp}\&quot;
		sparkle:shortVersionString=\&quot;#{@short_version.chomp}\&quot;
		type=\&quot;application/octet-stream\&quot;
		length=\&quot;#{@size}\&quot;
		sparkle:dsaSignature=\&quot;#{@signature.chomp}\&quot;
	/&gt;
    &lt;/item&gt;&quot;

    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 =~ /&lt;pubDate&gt;/
       		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 &quot;\n\n----------------------------------------------&quot;
    puts MESSAGE_HEADER
    puts msg
    puts &quot;----------------------------------------------\n\n&quot;
  end
end

if __FILE__ == $0
  newAppcast = AppCast.new
  newAppcast.main_worker_bee
  newAppcast.log_message(&quot;It appears all went well with the build script!&quot;)
end

Advertisement

Actions

Information

3 responses

30 01 2010
script binary

Thanks Brother for your share, nice info, and so usefull for me..
script mlm,script binary

30 01 2010
Blind Genius

No Probs Glad this helped someone ;)

17 03 2010
Ignasi

I tried using Olearia to listen to a Daisy book that I downloaded from the organization for the blind here in Spain. When I try to open it, it gave me a message saying that there was an error opening the control file. Please contact me at icambra@indiana.edu if you would like me to send you this book, so you can make Olearia compatible with Daisy books produced in Spain. There are thousands of them, and several people in Spain are now using Mac OS X and would probably benefit from using Olearia. Thanks!

Ignasi

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s




Follow

Get every new post delivered to your Inbox.