Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(71)

Side by Side Diff: remoting/tools/zip2msi.py

Issue 10959031: [Chromoting] Publish Chromoting Host installation as a .zip archive. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Oops, |target| and |sources| were swapped. Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « remoting/tools/dark_and_candle_and_light.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """ 6 """Generates .msi from a .zip archive or an unpacked directory.
7 Generates .msi from a .zip archive or an uppacked directory. The structure of 7
8 the input archive or directory should look like this: 8 The structure of the input archive or directory should look like this:
9 9
10 +- archive.zip 10 +- archive.zip
11 +- archive 11 +- archive
12 +- parameters.json 12 +- parameters.json
13 13
14 The name of the archive and the top level directory in the archive must match. 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 15 When an unpacked directory is used as the input "archive.zip/archive" should
16 be passed via the command line. 16 be passed via the command line.
17 17
18 'parameters.json' specifies the parameters to be passed to candle/light and 18 'parameters.json' specifies the parameters to be passed to candle/light and
19 must have the following structure: 19 must have the following structure:
20 20
21 { 21 {
22 "defines": { "name": "value" }, 22 "defines": { "name": "value" },
23 "extensions": [ "WixFirewallExtension.dll" ], 23 "extensions": [ "WixFirewallExtension.dll" ],
24 "switches": [ '-nologo' ], 24 "switches": [ '-nologo' ],
25 "source": "chromoting.wxs", 25 "source": "chromoting.wxs",
26 "bind_path": "files", 26 "bind_path": "files",
27 "sign": [ ... ],
27 "candle": { ... }, 28 "candle": { ... },
28 "light": { ... } 29 "light": { ... }
29 } 30 }
30 31
31 "source" specifies the name of the input .wxs relative to 32 "source" specifies the name of the input .wxs relative to
32 "archive.zip/archive". 33 "archive.zip/archive".
33 "bind_path" specifies the path where to look for binary files referenced by 34 "bind_path" specifies the path where to look for binary files referenced by
34 .wxs relative to "archive.zip/archive". 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.
35 """ 46 """
36 47
37 import copy 48 import copy
38 import json 49 import json
39 from optparse import OptionParser 50 from optparse import OptionParser
40 import os 51 import os
41 import re 52 import re
42 import subprocess 53 import subprocess
43 import sys 54 import sys
44 import zipfile 55 import zipfile
45 56
46 def extractZip(source, dest): 57
47 """ Extracts |source| ZIP archive to |dest| folder returning |True| if 58 def UnpackZip(target, source):
48 successful.""" 59 """Unpacks |source| archive to |target| directory."""
60 target = os.path.normpath(target)
49 archive = zipfile.ZipFile(source, 'r') 61 archive = zipfile.ZipFile(source, 'r')
50 for f in archive.namelist(): 62 for f in archive.namelist():
51 target = os.path.normpath(os.path.join(dest, f)) 63 target_file = os.path.normpath(os.path.join(target, f))
52 # Sanity check to make sure .zip uses relative paths. 64 # Sanity check to make sure .zip uses relative paths.
53 if os.path.commonprefix([target, dest]) != dest: 65 if os.path.commonprefix([target_file, target]) != target:
54 print "Failed to unpack '%s': '%s' is not under '%s'" % ( 66 print "Failed to unpack '%s': '%s' is not under '%s'" % (
55 source, target, dest) 67 source, target_file, target)
56 return False 68 return 1
57 69
58 # Create intermediate directories. 70 # Create intermediate directories.
59 target_dir = os.path.dirname(target) 71 target_dir = os.path.dirname(target_file)
60 if not os.path.exists(target_dir): 72 if not os.path.exists(target_dir):
61 os.makedirs(target_dir) 73 os.makedirs(target_dir)
62 74
63 archive.extract(f, dest) 75 archive.extract(f, target)
64 return True 76 return 0
65 77
66 def merge(left, right): 78
67 """ Merges to values. The result is: 79 def Merge(left, right):
80 """Merges two values.
81
82 Raises:
83 TypeError: |left| and |right| cannot be merged.
84
85 Returns:
68 - if both |left| and |right| are dictionaries, they are merged recursively. 86 - if both |left| and |right| are dictionaries, they are merged recursively.
69 - if both |left| and |right| are lists, the result is a list containing 87 - if both |left| and |right| are lists, the result is a list containing
70 elements from both lists. 88 elements from both lists.
71 - if both |left| and |right| are simple value, |right| is returned. 89 - if both |left| and |right| are simple value, |right| is returned.
72 - |TypeError| exception is raised if a dictionary or a list are merged with 90 - |TypeError| exception is raised if a dictionary or a list are merged with
73 a non-dictionary or non-list correspondingly. 91 a non-dictionary or non-list correspondingly.
74 """ 92 """
75 if isinstance(left, dict): 93 if isinstance(left, dict):
76 if isinstance(right, dict): 94 if isinstance(right, dict):
77 retval = copy.copy(left) 95 retval = copy.copy(left)
78 for key, value in right.iteritems(): 96 for key, value in right.iteritems():
79 if key in retval: 97 if key in retval:
80 retval[key] = merge(retval[key], value) 98 retval[key] = Merge(retval[key], value)
81 else: 99 else:
82 retval[key] = value 100 retval[key] = value
83 return retval 101 return retval
84 else: 102 else:
85 raise TypeError("Error: merging a dictionary and non-dictionary value") 103 raise TypeError('Error: merging a dictionary and non-dictionary value')
86 elif isinstance(left, list): 104 elif isinstance(left, list):
87 if isinstance(right, list): 105 if isinstance(right, list):
88 return left + right 106 return left + right
89 else: 107 else:
90 raise TypeError("Error: merging a list and non-list value") 108 raise TypeError('Error: merging a list and non-list value')
91 else: 109 else:
92 if isinstance(right, dict): 110 if isinstance(right, dict):
93 raise TypeError("Error: merging a dictionary and non-dictionary value") 111 raise TypeError('Error: merging a dictionary and non-dictionary value')
94 elif isinstance(right, list): 112 elif isinstance(right, list):
95 raise TypeError("Error: merging a dictionary and non-dictionary value") 113 raise TypeError('Error: merging a dictionary and non-dictionary value')
96 else: 114 else:
97 return right 115 return right
98 116
99 quote_matcher_regex = re.compile(r'\s|"') 117 quote_matcher_regex = re.compile(r'\s|"')
100 quote_replacer_regex = re.compile(r'(\\*)"') 118 quote_replacer_regex = re.compile(r'(\\*)"')
101 119
102 def quoteArgument(arg): 120
121 def QuoteArgument(arg):
103 """Escapes a Windows command-line argument. 122 """Escapes a Windows command-line argument.
104 123
105 So that the Win32 CommandLineToArgv function will turn the escaped result back 124 So that the Win32 CommandLineToArgv function will turn the escaped result back
106 into the original string. 125 into the original string.
107 See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx 126 See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
108 ("Parsing C++ Command-Line Arguments") to understand why we have to do 127 ("Parsing C++ Command-Line Arguments") to understand why we have to do
109 this. 128 this.
110 129
111 Args: 130 Args:
112 arg: the string to be escaped. 131 arg: the string to be escaped.
113 Returns: 132 Returns:
114 the escaped string. 133 the escaped string.
115 """ 134 """
116 135
117 def _Replace(match): 136 def _Replace(match):
118 # For a literal quote, CommandLineToArgv requires an odd number of 137 # For a literal quote, CommandLineToArgv requires an odd number of
119 # backslashes preceding it, and it produces half as many literal backslashes 138 # backslashes preceding it, and it produces half as many literal backslashes
120 # (rounded down). So we need to produce 2n+1 backslashes. 139 # (rounded down). So we need to produce 2n+1 backslashes.
121 return 2 * match.group(1) + '\\"' 140 return 2 * match.group(1) + '\\"'
122 141
123 if re.search(quote_matcher_regex, arg): 142 if re.search(quote_matcher_regex, arg):
124 # Escape all quotes so that they are interpreted literally. 143 # Escape all quotes so that they are interpreted literally.
125 arg = quote_replacer_regex.sub(_Replace, arg) 144 arg = quote_replacer_regex.sub(_Replace, arg)
126 # Now add unescaped quotes so that any whitespace is interpreted literally. 145 # Now add unescaped quotes so that any whitespace is interpreted literally.
127 return '"' + arg + '"' 146 return '"' + arg + '"'
128 else: 147 else:
129 return arg 148 return arg
130 149
131 def generateCommandLine(tool, source, dest, parameters): 150
151 def GenerateCommandLine(tool, source, dest, parameters):
132 """Generates the command line for |tool|.""" 152 """Generates the command line for |tool|."""
133 # Merge/apply tool-specific parameters 153 # Merge/apply tool-specific parameters
134 params = copy.copy(parameters) 154 params = copy.copy(parameters)
135 if tool in parameters: 155 if tool in parameters:
136 params = merge(params, params[tool]) 156 params = Merge(params, params[tool])
137 157
138 wix_path = os.path.normpath(params.get('wix_path', '')) 158 wix_path = os.path.normpath(params.get('wix_path', ''))
139 switches = [ os.path.join(wix_path, tool), '-nologo' ] 159 switches = [os.path.join(wix_path, tool), '-nologo']
140 160
141 # Append the list of defines and extensions to the command line switches. 161 # Append the list of defines and extensions to the command line switches.
142 for name, value in params.get('defines', {}).iteritems(): 162 for name, value in params.get('defines', {}).iteritems():
143 switches.append('-d%s=%s' % (name, value)) 163 switches.append('-d%s=%s' % (name, value))
144 164
145 for ext in params.get('extensions', []): 165 for ext in params.get('extensions', []):
146 switches += ('-ext', os.path.join(wix_path, ext)) 166 switches += ('-ext', os.path.join(wix_path, ext))
147 167
148 # Append raw switches 168 # Append raw switches
149 switches += params.get('switches', []) 169 switches += params.get('switches', [])
150 170
151 # Append the input and output files 171 # Append the input and output files
152 switches += ('-out', dest, source) 172 switches += ('-out', dest, source)
153 173
154 # Generate the actual command line 174 # Generate the actual command line
155 #return ' '.join(map(quoteArgument, switches)) 175 #return ' '.join(map(QuoteArgument, switches))
156 return switches 176 return switches
157 177
158 def run(args): 178
159 """ Constructs a quoted command line from the passed |args| list and runs it. 179 def Run(args):
160 Prints the exit code and output of the command if an error occurs.""" 180 """Runs a command interpreting the passed |args| as a command line."""
161 command = ' '.join(map(quoteArgument, args)) 181 command = ' '.join(map(QuoteArgument, args))
162 popen = subprocess.Popen( 182 popen = subprocess.Popen(
163 command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 183 command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
164 out, _ = popen.communicate() 184 out, _ = popen.communicate()
165 if popen.returncode: 185 if popen.returncode:
166 print command 186 print command
167 for line in out.splitlines(): 187 for line in out.splitlines():
168 print line 188 print line
169 print '%s returned %d' % (args[0], popen.returncode) 189 print '%s returned %d' % (args[0], popen.returncode)
170 return popen.returncode 190 return popen.returncode
171 191
172 def main():
173 usage = "Usage: zip2msi [options] <input.zip> <output.msi>"
174 parser = OptionParser(usage=usage)
175 parser.add_option('--intermediate_dir', dest='intermediate_dir')
176 parser.add_option('--wix_path', dest='wix_path')
177 options, args = parser.parse_args()
178 if len(args) != 2:
179 parser.error("two positional arguments expected")
180 parameters = dict(options.__dict__)
181 192
182 parameters['basename'] = os.path.splitext(os.path.basename(args[0]))[0] 193 def GenerateMsi(target, source, parameters):
183 if not options.intermediate_dir: 194 """Generates .msi from the installation files prepared by Chromium build."""
184 parameters['intermediate_dir'] = os.path.normpath('.') 195 parameters['basename'] = os.path.splitext(os.path.basename(source))[0]
185 196
186 # The script can handle both forms of input a directory with unpacked files or 197 # The script can handle both forms of input a directory with unpacked files or
187 # a ZIP archive with the same files. In the latter case the archive should be 198 # a ZIP archive with the same files. In the latter case the archive should be
188 # unpacked to the intermediate directory. 199 # unpacked to the intermediate directory.
189 intermediate_dir = os.path.normpath(parameters['intermediate_dir'])
190 source_dir = None 200 source_dir = None
191 if os.path.isdir(args[0]): 201 if os.path.isdir(source):
192 # Just use unpacked files from the supplied directory. 202 # Just use unpacked files from the supplied directory.
193 source_dir = args[0] 203 source_dir = source
194 else: 204 else:
195 # Unpack .zip 205 # Unpack .zip
196 if not extractZip(args[0], intermediate_dir): 206 rc = UnpackZip(parameters['intermediate_dir'], source)
197 return 1 207 if rc != 0:
208 return rc
198 source_dir = '%(intermediate_dir)s\\%(basename)s' % parameters 209 source_dir = '%(intermediate_dir)s\\%(basename)s' % parameters
199 210
200 # Read parameters from 'parameters.json'. 211 # Read parameters from 'parameters.json'.
201 f = open(os.path.join(source_dir, 'parameters.json')) 212 f = open(os.path.join(source_dir, 'parameters.json'))
202 parameters = merge(parameters, json.load(f)) 213 parameters = Merge(json.load(f), parameters)
203 f.close() 214 f.close()
204 215
205 if 'source' not in parameters: 216 if 'source' not in parameters:
206 print "The source .wxs is not specified" 217 print 'The source .wxs is not specified'
207 return 1 218 return 1
208 219
209 if 'bind_path' not in parameters: 220 if 'bind_path' not in parameters:
210 print "The binding path is not specified" 221 print 'The binding path is not specified'
211 return 1 222 return 1
212 223
213 dest = args[1] 224 wxs = os.path.join(source_dir, parameters['source'])
214 source = os.path.join(source_dir, parameters['source'])
215 225
216 # Add the binding path to the light-specific parameters. 226 # Add the binding path to the light-specific parameters.
217 bind_path = os.path.join(source_dir, parameters['bind_path']) 227 bind_path = os.path.join(source_dir, parameters['bind_path'])
218 parameters = merge(parameters, {'light': {'switches': ['-b', bind_path]}}) 228 parameters = Merge(parameters, {'light': {'switches': ['-b', bind_path]}})
219 229
220 # Run candle and light to generate the installation. 230 # Run candle and light to generate the installation.
221 wixobj = '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters 231 wixobj = '%(intermediate_dir)s\\%(basename)s.wixobj' % parameters
222 args = generateCommandLine('candle', source, wixobj, parameters) 232 args = GenerateCommandLine('candle', wxs, wixobj, parameters)
223 rc = run(args) 233 rc = Run(args)
224 if rc: 234 if rc:
225 return rc 235 return rc
226 236
227 args = generateCommandLine('light', wixobj, dest, parameters) 237 args = GenerateCommandLine('light', wixobj, target, parameters)
228 rc = run(args) 238 rc = Run(args)
229 if rc: 239 if rc:
230 return rc 240 return rc
231 241
232 return 0 242 return 0
233 243
234 if __name__ == "__main__": 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__':
235 sys.exit(main()) 257 sys.exit(main())
236 258
OLDNEW
« no previous file with comments | « remoting/tools/dark_and_candle_and_light.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698