1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
|
#!/usr/bin/env python
# Copyright 2012 The Chromium Authors
# 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.
"""
from __future__ import print_function
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.items():
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', {}).items():
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())
|