|  | #!/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()) | 
|  |  |