blob: e2e2537b2d5e6a64df6b8a32da3be7ad681ed95d [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates .msi from a .zip archive or an unpacked directory.
The structure of the input archive or directory should look like this:
+- archive.zip
+- archive
+- parameters.json
The name of the archive and the top level directory in the archive must match.
When an unpacked directory is used as the input "archive.zip/archive" should
be passed via the command line.
'parameters.json' specifies the parameters to be passed to candle/light and
must have the following structure:
{
"defines": { "name": "value" },
"extensions": [ "WixFirewallExtension.dll" ],
"switches": [ '-nologo' ],
"source": "chromoting.wxs",
"bind_path": "files",
"sign": [ ... ],
"candle": { ... },
"light": { ... }
}
"source" specifies the name of the input .wxs relative to
"archive.zip/archive".
"bind_path" specifies the path where to look for binary files referenced by
.wxs relative to "archive.zip/archive".
This script is used for both building Chromoting Host installation during
Chromuim build and for signing Chromoting Host installation later. There are two
copies of this script because of that:
- one in Chromium tree at src/remoting/tools/zip2msi.py.
- another one next to the signing scripts.
The copies of the script can be out of sync so make sure that a newer version is
compatible with the older ones when updating the script.
"""
import copy
import json
from optparse import OptionParser
import os
import re
import subprocess
import sys
import zipfile
def UnpackZip(target, source):
"""Unpacks |source| archive to |target| directory."""
target = os.path.normpath(target)
archive = zipfile.ZipFile(source, 'r')
for f in archive.namelist():
target_file = os.path.normpath(os.path.join(target, f))
# Sanity check to make sure .zip uses relative paths.
if os.path.commonprefix([target_file, target]) != target:
print "Failed to unpack '%s': '%s' is not under '%s'" % (
source, target_file, target)
return 1
# Create intermediate directories.
target_dir = os.path.dirname(target_file)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
archive.extract(f, target)
return 0
def Merge(left, right):
"""Merges two values.
Raises:
TypeError: |left| and |right| cannot be merged.
Returns:
- if both |left| and |right| are dictionaries, they are merged recursively.
- if both |left| and |right| are lists, the result is a list containing
elements from both lists.
- if both |left| and |right| are simple value, |right| is returned.
- |TypeError| exception is raised if a dictionary or a list are merged with
a non-dictionary or non-list correspondingly.
"""
if isinstance(left, dict):
if isinstance(right, dict):
retval = copy.copy(left)
for key, value in right.iteritems():
if key in retval:
retval[key] = Merge(retval[key], value)
else:
retval[key] = value
return retval
else:
raise TypeError('Error: merging a dictionary and non-dictionary value')
elif isinstance(left, list):
if isinstance(right, list):
return left + right
else:
raise TypeError('Error: merging a list and non-list value')
else:
if isinstance(right, dict):
raise TypeError('Error: merging a dictionary and non-dictionary value')
elif isinstance(right, list):
raise TypeError('Error: merging a dictionary and non-dictionary value')
else:
return right
quote_matcher_regex = re.compile(r'\s|"')
quote_replacer_regex = re.compile(r'(\\*)"')
def QuoteArgument(arg):
"""Escapes a Windows command-line argument.
So that the Win32 CommandLineToArgv function will turn the escaped result back
into the original string.
See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
("Parsing C++ Command-Line Arguments") to understand why we have to do
this.
Args:
arg: the string to be escaped.
Returns:
the escaped string.
"""
def _Replace(match):
# For a literal quote, CommandLineToArgv requires an odd number of
# backslashes preceding it, and it produces half as many literal backslashes
# (rounded down). So we need to produce 2n+1 backslashes.
return 2 * match.group(1) + '\\"'
if re.search(quote_matcher_regex, arg):
# Escape all quotes so that they are interpreted literally.
arg = quote_replacer_regex.sub(_Replace, arg)
# Now add unescaped quotes so that any whitespace is interpreted literally.
return '"' + arg + '"'
else:
return arg
def GenerateCommandLine(tool, source, dest, parameters):
"""Generates the command line for |tool|."""
# Merge/apply tool-specific parameters
params = copy.copy(parameters)
if tool in parameters:
params = Merge(params, params[tool])
wix_path = os.path.normpath(params.get('wix_path', ''))
switches = [os.path.join(wix_path, tool), '-nologo']
# Append the list of defines and extensions to the command line switches.
for name, value in params.get('defines', {}).iteritems():
switches.append('-d%s=%s' % (name, value))
for ext in params.get('extensions', []):
switches += ('-ext', os.path.join(wix_path, ext))
# Append raw switches
switches += params.get('switches', [])
# Append the input and output files
switches += ('-out', dest, source)
# Generate the actual command line
#return ' '.join(map(QuoteArgument, switches))
return switches
def Run(args):
"""Runs a command interpreting the passed |args| as a command line."""
command = ' '.join(map(QuoteArgument, args))
popen = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = popen.communicate()
if popen.returncode:
print command
for line in out.splitlines():
print line
print '%s returned %d' % (args[0], popen.returncode)
return popen.returncode
def GenerateMsi(target, source, parameters):
"""Generates .msi from the installation files prepared by Chromium build."""
parameters['basename'] = os.path.splitext(os.path.basename(source))[0]
# The script can handle both forms of input a directory with unpacked files or
# a ZIP archive with the same files. In the latter case the archive should be
# unpacked to the intermediate directory.
source_dir = None
if os.path.isdir(source):
# Just use unpacked files from the supplied directory.
source_dir = source
else:
# Unpack .zip
rc = UnpackZip(parameters['intermediate_dir'], source)
if rc != 0:
return rc
source_dir = '%(intermediate_dir)s\\%(basename)s' % parameters
# Read parameters from 'parameters.json'.
f = open(os.path.join(source_dir, 'parameters.json'))
parameters = Merge(json.load(f), parameters)
f.close()
if 'source' not in parameters:
print 'The source .wxs is not specified'
return 1
if 'bind_path' not in parameters:
print 'The binding path is not specified'
return 1
wxs = os.path.join(source_dir, parameters['source'])
# Add the binding path to the light-specific parameters.
bind_path = os.path.join(source_dir, parameters['bind_path'])
parameters = Merge(parameters, {'light': {'switches': ['-b', bind_path]}})
target_arch = parameters['target_arch']
if target_arch == 'ia32':
arch_param = 'x86'
elif target_arch == 'x64':
arch_param = 'x64'
else:
print 'Invalid target_arch parameter value'
return 1
# Add the architecture to candle-specific parameters.
parameters = Merge(
parameters, {'candle': {'switches': ['-arch', arch_param]}})
# Run candle and light to generate the installation.
wixobj = '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters
args = GenerateCommandLine('candle', wxs, wixobj, parameters)
rc = Run(args)
if rc:
return rc
args = GenerateCommandLine('light', wixobj, target, parameters)
rc = Run(args)
if rc:
return rc
return 0
def main():
usage = 'Usage: zip2msi [options] <input.zip> <output.msi>'
parser = OptionParser(usage=usage)
parser.add_option('--intermediate_dir', dest='intermediate_dir', default='.')
parser.add_option('--wix_path', dest='wix_path', default='.')
parser.add_option('--target_arch', dest='target_arch', default='x86')
options, args = parser.parse_args()
if len(args) != 2:
parser.error('two positional arguments expected')
return GenerateMsi(args[1], args[0], dict(options.__dict__))
if __name__ == '__main__':
sys.exit(main())