diff --git a/tools/README b/tools/README
new file mode 100644
index 0000000..ff8cc5e
--- /dev/null
+++ b/tools/README
@@ -0,0 +1,14 @@
+This directory contains a set of tools for preparing auto-updates.
+
+ * create-packages.rb
+
+ Given a directory containing the set of files that make up a release,
+ laid out as they are when installed and a JSON file mapping files
+ to packages, this tool generates a set of packages for the release
+ and a file_list.xml listing all the files that make up the release.
+
+ * single-package-map.json
+
+ This is the simplest possible package map, where all files for
+ a release are placed in a single package. This means that the whole
+ package will need to be downloaded to install the update.
diff --git a/tools/create-packages.rb b/tools/create-packages.rb
new file mode 100755
index 0000000..aa83ddf
--- /dev/null
+++ b/tools/create-packages.rb
@@ -0,0 +1,317 @@
+#!/usr/bin/ruby
+
+require 'rubygems'
+require 'find'
+require 'json'
+require 'rexml/document'
+require 'optparse'
+
+# syntax:
+#
+# create-packages.rb
+#
+# Takes the set of files that make up a release and splits them up into
+# a set of .zip packages
+#
+# Outputs:
+#
+# /$PACKAGE_NAME.zip
+# These are packages containing the files in this version of the software.
+#
+# /file_list.xml
+# This file lists all the files contained in this version of the software and
+# the packages which they are contained in.
+#
+
+# Represents a group of updates in a release
+class UpdateScriptPackage
+ # name - The name of the package (without any extension)
+ # hash - The SHA-1 hash of the package
+ # size - The size of the package in bytes
+ attr_reader :name,:hash,:size
+ attr_writer :name,:hash,:size
+end
+
+# Represents a single file in a release
+class UpdateScriptFile
+ # path - The path of the file relative to the installation directory
+ # hash - The SHA-1 hash of the file
+ # permissions - The permissions of the file expressed using
+ # flags from the QFile::Permission enum in Qt
+ # size - The size of the file in bytes
+ # package - The name of the package containing this file
+ attr_reader :path,:hash,:permissions,:size,:package,:target
+ attr_writer :path,:hash,:permissions,:size,:package,:target
+end
+
+# Utility method - convert a hash map to an REXML element
+#
+# Hash keys are converted to elements and hash values either
+# to text contents or child elements (if the value is a Hash)
+#
+# 'root' - the root REXML::Element
+# 'map' - a hash mapping element names to text contents or
+# hash maps
+#
+def hash_to_xml(root,map)
+ map.each do |key,value|
+ element = REXML::Element.new(key)
+ if value.instance_of?(String)
+ element.text = value
+ elsif value.instance_of?(Hash)
+ hash_to_xml(element,value)
+ elsif !value.nil?
+ raise "Unsupported value type #{value.class}"
+ end
+ root.add_element element
+ end
+ return root
+end
+
+def strip_prefix(string,prefix)
+ if (!string.start_with?(prefix))
+ raise "String does not start with prefix"
+ end
+ return string.sub(prefix,"")
+end
+
+class UpdateScriptGenerator
+ def initialize(input_dir,output_dir, file_list, package_file_map)
+ # List of files to install in this version
+ @files_to_install = []
+ file_list.each do |path|
+ file = UpdateScriptFile.new
+ file.path = strip_prefix(path,input_dir)
+
+ if (File.symlink?(path))
+ file.target = File.readlink(path)
+ else
+ file.hash = file_sha1(path)
+ file.permissions = get_file_permissions(path)
+ file.size = File.size(path)
+ package_file_map.each do |package,files|
+ if files.include?(path)
+ file.package = package
+ break
+ end
+ end
+ end
+
+ @files_to_install << file
+ end
+
+ # List of packages containing files for this version
+ @packages = []
+ package_file_map.each do |package_name,files|
+ path = "#{output_dir}/#{package_name}.zip"
+ package = UpdateScriptPackage.new
+ package.name = package_name
+ package.size = File.size(path)
+ package.hash = file_sha1(path)
+ @packages << package
+ end
+ end
+
+ def toXML()
+ doc = REXML::Document.new
+ update_elem = REXML::Element.new("update")
+ doc.add_element update_elem
+
+ update_elem.add_element deps_to_xml()
+ update_elem.add_element packages_to_xml()
+ update_elem.add_element install_to_xml()
+
+ output = ""
+ doc.write output
+ return output
+ end
+
+ def deps_to_xml()
+ deps_elem = REXML::Element.new("dependencies")
+ deps = ["updater.exe"]
+ deps.each do |dependency|
+ dep_elem = REXML::Element.new("file")
+ dep_elem.text = dependency
+ deps_elem.add_element dep_elem
+ end
+ return deps_elem
+ end
+
+ def packages_to_xml()
+ packages_elem = REXML::Element.new("packages")
+ @packages.each do |package|
+ package_elem = REXML::Element.new("package")
+ packages_elem.add_element package_elem
+ hash_to_xml(package_elem,{
+ "name" => package.name,
+ "size" => package.size.to_s,
+ "hash" => package.hash
+ })
+ end
+ return packages_elem
+ end
+
+ def install_to_xml()
+ install_elem = REXML::Element.new("install")
+ @files_to_install.each do |file|
+ file_elem = REXML::Element.new("file")
+ install_elem.add_element(file_elem)
+
+ attributes = {"name" => file.path}
+ if (file.target)
+ attributes["target"] = file.target
+ else
+ attributes["size"] = file.size.to_s
+ attributes["permissions"] = file.permissions.to_s
+ attributes["hash"] = file.hash
+ attributes["package"] = file.package
+ end
+ hash_to_xml(file_elem,attributes)
+ end
+ return install_elem
+ end
+
+ def file_sha1(path)
+ return `sha1sum "#{path}"`.split(' ')[0]
+ end
+
+ # Unix permission flags
+ # from
+ S_IRUSR = 0400
+ S_IWUSR = 0200
+ S_IXUSR = 0100
+ S_IRGRP = (S_IRUSR >> 3)
+ S_IWGRP = (S_IWUSR >> 3)
+ S_IXGRP = (S_IXUSR >> 3)
+ S_IROTH = (S_IRGRP >> 3)
+ S_IWOTH = (S_IWGRP >> 3)
+ S_IXOTH = (S_IXGRP >> 3)
+
+ # Qt permission flags
+ # (taken from QFile::Permission)
+ QT_READ_OWNER = 0x4000
+ QT_WRITE_OWNER = 0x2000
+ QT_EXEC_OWNER = 0x1000
+ QT_READ_USER = 0x0400
+ QT_WRITE_USER = 0x0200
+ QT_EXEC_USER = 0x0100
+ QT_READ_GROUP = 0x0040
+ QT_WRITE_GROUP = 0x0020
+ QT_EXEC_GROUP = 0x0010
+ QT_READ_OTHER = 0x0004
+ QT_WRITE_OTHER = 0x0002
+ QT_EXEC_OTHER = 0x0001
+
+ def get_file_permissions(path)
+ unix_to_qt = {
+ S_IRUSR => QT_READ_USER | QT_READ_OWNER,
+ S_IWUSR => QT_WRITE_USER | QT_WRITE_OWNER,
+ S_IXUSR => QT_EXEC_USER | QT_EXEC_OWNER,
+ S_IRGRP => QT_READ_GROUP,
+ S_IWGRP => QT_WRITE_GROUP,
+ S_IXGRP => QT_EXEC_GROUP,
+ S_IROTH => QT_READ_OTHER,
+ S_IWOTH => QT_WRITE_OTHER,
+ S_IXOTH => QT_EXEC_OTHER
+ }
+
+ qt_permissions = 0
+ unix_permissions = File.stat(path).mode
+ unix_to_qt.each do |unix_flag,qt_flags|
+ qt_permissions |= qt_flags if ((unix_permissions & unix_flag) != 0)
+ end
+
+ return qt_permissions.to_i
+ end
+end
+
+class PackageMap
+ def initialize(map_file)
+ @rule_map = {}
+ map_json = JSON.parse(File.read(map_file))
+ map_json.each do |package,rules|
+ rules.each do |rule|
+ rule_regex = Regexp.new(rule)
+ @rule_map[rule_regex] = package
+ end
+ end
+ end
+
+ def package_for_file(file)
+ @rule_map.each do |rule,package|
+ if (file =~ rule)
+ return package
+ end
+ end
+ return nil
+ end
+end
+
+OptionParser.new do |parser|
+ parser.banner = "#{$0} "
+end.parse!
+
+if ARGV.length < 3
+ raise "Missing arguments"
+end
+
+input_dir = ARGV[0]
+package_map_file = ARGV[1]
+output_dir = ARGV[2]
+
+# get the details of each input file
+input_file_list = []
+Find.find(input_dir) do |path|
+ next if (File.directory?(path))
+ input_file_list << path
+end
+
+# map each input file to a corresponding package
+
+# read the package map
+package_map = PackageMap.new(package_map_file)
+
+# map of package name -> array of files
+package_file_map = {}
+input_file_list.each do |file|
+ next if File.symlink?(file)
+
+ package = package_map.package_for_file(file)
+ if (!package)
+ raise "Unable to find package for file #{file}"
+ end
+ package_file_map[package] = [] if !package_file_map[package]
+ package_file_map[package] << file
+end
+
+# generate each package
+package_file_map.each do |package,files|
+ puts "Generating package #{package}"
+
+ quoted_files = []
+ files.each do |file|
+ quoted_files << "\"#{strip_prefix(file,input_dir)}\""
+ end
+ quoted_file_list = quoted_files.join(" ")
+
+ output_path = File.expand_path(output_dir)
+ Dir.chdir(input_dir) do
+#if (!system("zip #{output_path}/#{package}.zip #{quoted_file_list}"))
+# raise "Failed to generate package #{package}"
+# end
+ end
+end
+
+# output the file_list.xml file
+update_script = UpdateScriptGenerator.new(input_dir,output_dir,input_file_list,package_file_map)
+output_xml_file = "#{output_dir}/file_list.unformatted.xml"
+File.open(output_xml_file,'w') do |file|
+ file.write update_script.toXML()
+end
+
+# xmllint generates more readable formatted XML than REXML, so write unformatted
+# XML first and then format it with xmllint.
+system("xmllint --format #{output_xml_file} > #{output_dir}/file_list.xml")
+File.delete(output_xml_file)
+
+
diff --git a/tools/single-package-map.json b/tools/single-package-map.json
new file mode 100644
index 0000000..97ed49e
--- /dev/null
+++ b/tools/single-package-map.json
@@ -0,0 +1,5 @@
+{
+ "app" : [
+ ".*"
+ ]
+}