Update-Installer/tools/create-packages.rb
Robert Knight 38bb0e8787 Add --bzip2 option to create-packages.rb to use bzip2 compression instead of deflate when creating the .zip packages
This results in smaller packages but the package creation will be slower
and the system 'zip' tool must support bzip2 compression.
2011-09-05 16:33:28 +01:00

401 lines
11 KiB
Ruby
Executable file

#!/usr/bin/ruby
require 'fileutils'
require 'rubygems'
require 'find'
require 'json'
require 'rexml/document'
require 'optparse'
# syntax:
#
# create-packages.rb <input directory> <config file> <output directory>
#
# Takes the set of files that make up a release and splits them up into
# a set of .zip packages along with a file_list.xml file listing all
# the files in the release and mapping them to their respective
# packages.
#
# Outputs:
#
# <output-dir>/$PACKAGE_NAME.zip
# These are packages containing the files in this version of the software.
#
# <output-dir>/file_list.xml
# This file lists all the files contained in this version of the software and
# the packages which they are contained in.
#
# <output-dir>/$UPDATER_BINARY
# The standalone auto-update installer binary. This is not compressed so that
# it can be downloaded and executed directly.
#
# 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 (See File::stat)
# 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,:is_main_binary
attr_writer :path,:hash,:permissions,:size,:package,:target,:is_main_binary
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
def file_sha1(path)
return `sha1sum "#{path}"`.split(' ')[0]
end
class UpdateScriptGenerator
# target_version - The version string for the build in this update
# platform - The platform which this build is for
# input_dir - The directory containing files that make up the install
# output_dir - The directory containing the generated packages
# package_config - The PackageConfig specifying the file -> package map
# for the application and other config options
# file_list - A list of all files in 'input_dir' which make up the install
# package_file_map - A map of (package name -> [paths of files in this package])
def initialize(target_version,
platform,
input_dir,
output_dir,
package_config,
file_list,
package_file_map)
@target_version = target_version
@platform = platform
@config = package_config
# 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.basename(path) == @config.updater_binary)
# for the updater binary, use the possibly substituted
# version in the output directory
path = "#{output_dir}/#{@config.updater_binary}"
end
file.is_main_binary = (file.path == package_config.main_binary)
if (File.symlink?(path))
file.target = File.readlink(path)
else
file.hash = file_sha1(path)
file.permissions = File.stat(path).mode
file.size = File.size(path)
file.package = @config.package_for_file(file.path)
if (!file.package)
raise "Could not find package for file #{file.path}"
end
end
@files_to_install << file
end
# List of packages containing files for this version
@packages = []
package_file_map.each do |package_name,files|
if (package_name == @config.updater_binary)
path = "#{output_dir}/#{package_name}"
else
path = "#{output_dir}/#{package_name}.zip"
end
package = UpdateScriptPackage.new
package.name = package_name
package.size = File.size(path)
package.hash = file_sha1(path)
@packages << package
end
end
def to_xml()
doc = REXML::Document.new
update_elem = REXML::Element.new("update")
doc.add_element update_elem
version_elem = REXML::Element.new("targetVersion")
version_elem.text = @target_version
platform_elem = REXML::Element.new("platform")
platform_elem.text = @platform
update_elem.add_attribute("version","3")
update_elem.add_element version_elem
update_elem.add_element platform_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 = @config.updater_binary
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_mode_string(file.permissions)
attributes["hash"] = file.hash
attributes["package"] = file.package
end
if (file.is_main_binary)
attributes["is-main-binary"] = "true"
end
hash_to_xml(file_elem,attributes)
end
return install_elem
end
def file_mode_string(mode)
mode_octal = (mode & 0777).to_s(8)
return "0#{mode_octal}"
end
end
class PackageConfig
attr_reader :main_binary, :updater_binary
def initialize(map_file)
@rule_map = {}
config_json = JSON.parse(File.read(map_file))
config_json["packages"].each do |package,rules|
rules.each do |rule|
rule_regex = Regexp.new("^#{rule}")
@rule_map[rule_regex] = package
end
end
@main_binary = config_json["main-binary"]
@updater_binary = config_json["updater-binary"]
end
def is_updater(file)
return File.basename(file) == @updater_binary
end
def package_for_file(file)
match = nil
@rule_map.each do |rule,package|
if (file =~ rule)
if match && match != package
raise "Multiple packages match file #{file} - found '#{match}' and '#{package}'"
end
match = package
end
end
return match
end
end
updater_binary_input_path = nil
target_version = nil
target_platform = nil
zip_flags = nil
OptionParser.new do |parser|
parser.banner = "#{$0} [options] <input dir> <config file> <output dir>"
parser.on("-u","--updater [updater binary]","Specifies the updater binary to use") do |updater|
updater_binary_input_path = updater
end
parser.on("-v","--version [version]","Specifies the target version string for this update") do |version|
target_version = version
end
parser.on("-p","--platform [platform]","Specifies the target platform for this update") do |platform|
target_platform = platform
end
parser.on(nil,"--bzip2","Use bzip2 compression (requires that 'zip' supports the -Z bz2 argument)") do
zip_flags = "-Z bzip2"
end
end.parse!
raise "Platform not specified (use -p option)" if !target_platform
raise "Target version not specified (use -v option)" if !target_version
if ARGV.length < 3
raise "Missing arguments"
end
input_dir = ARGV[0]
package_map_file = ARGV[1]
output_dir = ARGV[2]
FileUtils.mkpath(output_dir)
# get the details of each input file
input_file_list = []
Find.find(input_dir) do |path|
next if (File.directory?(path) && !File.symlink?(path))
input_file_list << path
end
# map each input file to a corresponding package
# read the package map
package_config = PackageConfig.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)
relative_file = strip_prefix(file,input_dir)
package = package_config.package_for_file(relative_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|
# do not package the updater binary into a zip file -
# it must be downloaded uncompressed
if package_config.is_updater(file)
next
end
quoted_files << "\"#{strip_prefix(file,input_dir)}\""
end
if (quoted_files.empty?)
puts "Skipping generation of empty package #{package}"
next
end
# sort the files in alphabetical order to ensure
# that if the input files for a package have the same
# name and content, the resulting package will have the
# same SHA-1
#
# This means that repeated runs of the package creation tool
# on the same set of files should generated packages with the
# same SHA-1
quoted_files.sort! do |a,b|
a <=> b
end
quoted_file_list = quoted_files.join(" ")
output_path = File.expand_path(output_dir)
output_file = "#{output_path}/#{package}.zip"
File.unlink(output_file) if File.exist?(output_file)
Dir.chdir(input_dir) do
if (!system("zip #{zip_flags} #{output_file} #{quoted_file_list}"))
raise "Failed to generate package #{package}"
else
puts "Generated package #{package} : #{file_sha1(output_file)}"
end
end
end
# copy the updater to the output directory
if !updater_binary_input_path
puts "Updater binary not found in input directory: #{input_dir}"
exit(1)
end
FileUtils.cp(updater_binary_input_path,"#{output_dir}/#{File.basename(updater_binary_input_path)}")
# output the file_list.xml file
update_script = UpdateScriptGenerator.new(target_version,target_platform,input_dir,
output_dir,package_config,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.to_xml()
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)