Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / tools / zip2msi.py
blobe2e2537b2d5e6a64df6b8a32da3be7ad681ed95d
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Generates .msi from a .zip archive or an unpacked directory.
8 The structure of the input archive or directory should look like this:
10 +- archive.zip
11 +- archive
12 +- parameters.json
14 The name of the archive and the top level directory in the archive must match.
15 When an unpacked directory is used as the input "archive.zip/archive" should
16 be passed via the command line.
18 'parameters.json' specifies the parameters to be passed to candle/light and
19 must have the following structure:
22 "defines": { "name": "value" },
23 "extensions": [ "WixFirewallExtension.dll" ],
24 "switches": [ '-nologo' ],
25 "source": "chromoting.wxs",
26 "bind_path": "files",
27 "sign": [ ... ],
28 "candle": { ... },
29 "light": { ... }
32 "source" specifies the name of the input .wxs relative to
33 "archive.zip/archive".
34 "bind_path" specifies the path where to look for binary files referenced by
35 .wxs relative to "archive.zip/archive".
37 This script is used for both building Chromoting Host installation during
38 Chromuim build and for signing Chromoting Host installation later. There are two
39 copies of this script because of that:
41 - one in Chromium tree at src/remoting/tools/zip2msi.py.
42 - another one next to the signing scripts.
44 The copies of the script can be out of sync so make sure that a newer version is
45 compatible with the older ones when updating the script.
46 """
48 import copy
49 import json
50 from optparse import OptionParser
51 import os
52 import re
53 import subprocess
54 import sys
55 import zipfile
58 def UnpackZip(target, source):
59 """Unpacks |source| archive to |target| directory."""
60 target = os.path.normpath(target)
61 archive = zipfile.ZipFile(source, 'r')
62 for f in archive.namelist():
63 target_file = os.path.normpath(os.path.join(target, f))
64 # Sanity check to make sure .zip uses relative paths.
65 if os.path.commonprefix([target_file, target]) != target:
66 print "Failed to unpack '%s': '%s' is not under '%s'" % (
67 source, target_file, target)
68 return 1
70 # Create intermediate directories.
71 target_dir = os.path.dirname(target_file)
72 if not os.path.exists(target_dir):
73 os.makedirs(target_dir)
75 archive.extract(f, target)
76 return 0
79 def Merge(left, right):
80 """Merges two values.
82 Raises:
83 TypeError: |left| and |right| cannot be merged.
85 Returns:
86 - if both |left| and |right| are dictionaries, they are merged recursively.
87 - if both |left| and |right| are lists, the result is a list containing
88 elements from both lists.
89 - if both |left| and |right| are simple value, |right| is returned.
90 - |TypeError| exception is raised if a dictionary or a list are merged with
91 a non-dictionary or non-list correspondingly.
92 """
93 if isinstance(left, dict):
94 if isinstance(right, dict):
95 retval = copy.copy(left)
96 for key, value in right.iteritems():
97 if key in retval:
98 retval[key] = Merge(retval[key], value)
99 else:
100 retval[key] = value
101 return retval
102 else:
103 raise TypeError('Error: merging a dictionary and non-dictionary value')
104 elif isinstance(left, list):
105 if isinstance(right, list):
106 return left + right
107 else:
108 raise TypeError('Error: merging a list and non-list value')
109 else:
110 if isinstance(right, dict):
111 raise TypeError('Error: merging a dictionary and non-dictionary value')
112 elif isinstance(right, list):
113 raise TypeError('Error: merging a dictionary and non-dictionary value')
114 else:
115 return right
117 quote_matcher_regex = re.compile(r'\s|"')
118 quote_replacer_regex = re.compile(r'(\\*)"')
121 def QuoteArgument(arg):
122 """Escapes a Windows command-line argument.
124 So that the Win32 CommandLineToArgv function will turn the escaped result back
125 into the original string.
126 See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
127 ("Parsing C++ Command-Line Arguments") to understand why we have to do
128 this.
130 Args:
131 arg: the string to be escaped.
132 Returns:
133 the escaped string.
136 def _Replace(match):
137 # For a literal quote, CommandLineToArgv requires an odd number of
138 # backslashes preceding it, and it produces half as many literal backslashes
139 # (rounded down). So we need to produce 2n+1 backslashes.
140 return 2 * match.group(1) + '\\"'
142 if re.search(quote_matcher_regex, arg):
143 # Escape all quotes so that they are interpreted literally.
144 arg = quote_replacer_regex.sub(_Replace, arg)
145 # Now add unescaped quotes so that any whitespace is interpreted literally.
146 return '"' + arg + '"'
147 else:
148 return arg
151 def GenerateCommandLine(tool, source, dest, parameters):
152 """Generates the command line for |tool|."""
153 # Merge/apply tool-specific parameters
154 params = copy.copy(parameters)
155 if tool in parameters:
156 params = Merge(params, params[tool])
158 wix_path = os.path.normpath(params.get('wix_path', ''))
159 switches = [os.path.join(wix_path, tool), '-nologo']
161 # Append the list of defines and extensions to the command line switches.
162 for name, value in params.get('defines', {}).iteritems():
163 switches.append('-d%s=%s' % (name, value))
165 for ext in params.get('extensions', []):
166 switches += ('-ext', os.path.join(wix_path, ext))
168 # Append raw switches
169 switches += params.get('switches', [])
171 # Append the input and output files
172 switches += ('-out', dest, source)
174 # Generate the actual command line
175 #return ' '.join(map(QuoteArgument, switches))
176 return switches
179 def Run(args):
180 """Runs a command interpreting the passed |args| as a command line."""
181 command = ' '.join(map(QuoteArgument, args))
182 popen = subprocess.Popen(
183 command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
184 out, _ = popen.communicate()
185 if popen.returncode:
186 print command
187 for line in out.splitlines():
188 print line
189 print '%s returned %d' % (args[0], popen.returncode)
190 return popen.returncode
193 def GenerateMsi(target, source, parameters):
194 """Generates .msi from the installation files prepared by Chromium build."""
195 parameters['basename'] = os.path.splitext(os.path.basename(source))[0]
197 # The script can handle both forms of input a directory with unpacked files or
198 # a ZIP archive with the same files. In the latter case the archive should be
199 # unpacked to the intermediate directory.
200 source_dir = None
201 if os.path.isdir(source):
202 # Just use unpacked files from the supplied directory.
203 source_dir = source
204 else:
205 # Unpack .zip
206 rc = UnpackZip(parameters['intermediate_dir'], source)
207 if rc != 0:
208 return rc
209 source_dir = '%(intermediate_dir)s\\%(basename)s' % parameters
211 # Read parameters from 'parameters.json'.
212 f = open(os.path.join(source_dir, 'parameters.json'))
213 parameters = Merge(json.load(f), parameters)
214 f.close()
216 if 'source' not in parameters:
217 print 'The source .wxs is not specified'
218 return 1
220 if 'bind_path' not in parameters:
221 print 'The binding path is not specified'
222 return 1
224 wxs = os.path.join(source_dir, parameters['source'])
226 # Add the binding path to the light-specific parameters.
227 bind_path = os.path.join(source_dir, parameters['bind_path'])
228 parameters = Merge(parameters, {'light': {'switches': ['-b', bind_path]}})
230 target_arch = parameters['target_arch']
231 if target_arch == 'ia32':
232 arch_param = 'x86'
233 elif target_arch == 'x64':
234 arch_param = 'x64'
235 else:
236 print 'Invalid target_arch parameter value'
237 return 1
239 # Add the architecture to candle-specific parameters.
240 parameters = Merge(
241 parameters, {'candle': {'switches': ['-arch', arch_param]}})
243 # Run candle and light to generate the installation.
244 wixobj = '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters
245 args = GenerateCommandLine('candle', wxs, wixobj, parameters)
246 rc = Run(args)
247 if rc:
248 return rc
250 args = GenerateCommandLine('light', wixobj, target, parameters)
251 rc = Run(args)
252 if rc:
253 return rc
255 return 0
258 def main():
259 usage = 'Usage: zip2msi [options] <input.zip> <output.msi>'
260 parser = OptionParser(usage=usage)
261 parser.add_option('--intermediate_dir', dest='intermediate_dir', default='.')
262 parser.add_option('--wix_path', dest='wix_path', default='.')
263 parser.add_option('--target_arch', dest='target_arch', default='x86')
264 options, args = parser.parse_args()
265 if len(args) != 2:
266 parser.error('two positional arguments expected')
268 return GenerateMsi(args[1], args[0], dict(options.__dict__))
270 if __name__ == '__main__':
271 sys.exit(main())