| OLD | NEW |
| 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 |
| OLD | NEW |