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:
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",
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.
50 from optparse
import OptionParser
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
)
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
)
79 def Merge(left
, right
):
83 TypeError: |left| and |right| cannot be merged.
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.
93 if isinstance(left
, dict):
94 if isinstance(right
, dict):
95 retval
= copy
.copy(left
)
96 for key
, value
in right
.iteritems():
98 retval
[key
] = Merge(retval
[key
], value
)
103 raise TypeError('Error: merging a dictionary and non-dictionary value')
104 elif isinstance(left
, list):
105 if isinstance(right
, list):
108 raise TypeError('Error: merging a list and non-list value')
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')
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
131 arg: the string to be escaped.
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
+ '"'
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))
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()
187 for line
in out
.splitlines():
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.
201 if os
.path
.isdir(source
):
202 # Just use unpacked files from the supplied directory.
206 rc
= UnpackZip(parameters
['intermediate_dir'], source
)
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
)
216 if 'source' not in parameters
:
217 print 'The source .wxs is not specified'
220 if 'bind_path' not in parameters
:
221 print 'The binding path is not specified'
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 # Run candle and light to generate the installation.
231 wixobj
= '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters
232 args
= GenerateCommandLine('candle', wxs
, wixobj
, parameters
)
237 args
= GenerateCommandLine('light', wixobj
, target
, parameters
)
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()
252 parser
.error('two positional arguments expected')
254 return GenerateMsi(args
[1], args
[0], dict(options
.__dict
__))
256 if __name__
== '__main__':