普通文本  |  259行  |  8.06 KB

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

  # 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='.')
  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())