- add sources.
[platform/framework/web/crosswalk.git] / src / remoting / tools / zip2msi.py
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.
5
6 """Generates .msi from a .zip archive or an unpacked directory.
7
8 The structure of the input archive or directory should look like this:
9
10   +- archive.zip
11      +- archive
12         +- parameters.json
13
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.
17
18 'parameters.json' specifies the parameters to be passed to candle/light and
19 must have the following structure:
20
21   {
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": { ... }
30   }
31
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".
36
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:
40
41   - one in Chromium tree at src/remoting/tools/zip2msi.py.
42   - another one next to the signing scripts.
43
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 """
47
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
56
57
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
69
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)
74
75     archive.extract(f, target)
76   return 0
77
78
79 def Merge(left, right):
80   """Merges two values.
81
82   Raises:
83     TypeError: |left| and |right| cannot be merged.
84
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
116
117 quote_matcher_regex = re.compile(r'\s|"')
118 quote_replacer_regex = re.compile(r'(\\*)"')
119
120
121 def QuoteArgument(arg):
122   """Escapes a Windows command-line argument.
123
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.
129
130   Args:
131       arg: the string to be escaped.
132   Returns:
133       the escaped string.
134   """
135
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) + '\\"'
141
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
149
150
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])
157
158   wix_path = os.path.normpath(params.get('wix_path', ''))
159   switches = [os.path.join(wix_path, tool), '-nologo']
160
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))
164
165   for ext in params.get('extensions', []):
166     switches += ('-ext', os.path.join(wix_path, ext))
167
168   # Append raw switches
169   switches += params.get('switches', [])
170
171   # Append the input and output files
172   switches += ('-out', dest, source)
173
174   # Generate the actual command line
175   #return ' '.join(map(QuoteArgument, switches))
176   return switches
177
178
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
191
192
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]
196
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
210
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()
215
216   if 'source' not in parameters:
217     print 'The source .wxs is not specified'
218     return 1
219
220   if 'bind_path' not in parameters:
221     print 'The binding path is not specified'
222     return 1
223
224   wxs = os.path.join(source_dir, parameters['source'])
225
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]}})
229
230   # Run candle and light to generate the installation.
231   wixobj = '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters
232   args = GenerateCommandLine('candle', wxs, wixobj, parameters)
233   rc = Run(args)
234   if rc:
235     return rc
236
237   args = GenerateCommandLine('light', wixobj, target, parameters)
238   rc = Run(args)
239   if rc:
240     return rc
241
242   return 0
243
244
245 def main():
246   usage = 'Usage: zip2msi [options] <input.zip> <output.msi>'
247   parser = OptionParser(usage=usage)
248   parser.add_option('--intermediate_dir', dest='intermediate_dir', default='.')
249   parser.add_option('--wix_path', dest='wix_path', default='.')
250   options, args = parser.parse_args()
251   if len(args) != 2:
252     parser.error('two positional arguments expected')
253
254   return GenerateMsi(args[1], args[0], dict(options.__dict__))
255
256 if __name__ == '__main__':
257   sys.exit(main())
258