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