Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # | 2 # |
| 3 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 3 # Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
| 6 | 6 |
| 7 """Updates the Chrome reference builds. | 7 """Updates the Chrome reference builds. |
| 8 | 8 |
| 9 Use -r option to update a Chromium reference build, or -b option for Chrome | 9 Use -r option to update a Chromium reference build, or -b option for Chrome |
| 10 official builds. | 10 official builds. |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 27 import time | 27 import time |
| 28 import urllib | 28 import urllib |
| 29 import urllib2 | 29 import urllib2 |
| 30 import zipfile | 30 import zipfile |
| 31 | 31 |
| 32 # Example chromium build location: | 32 # Example chromium build location: |
| 33 # gs://chromium-browser-snapshots/Linux/228977/chrome-linux.zip | 33 # gs://chromium-browser-snapshots/Linux/228977/chrome-linux.zip |
| 34 CHROMIUM_URL_FMT = ('http://commondatastorage.googleapis.com/' | 34 CHROMIUM_URL_FMT = ('http://commondatastorage.googleapis.com/' |
| 35 'chromium-browser-snapshots/%s/%s/%s') | 35 'chromium-browser-snapshots/%s/%s/%s') |
| 36 | 36 |
| 37 # Example Chrome build location (no public wed URL's): | 37 # Chrome official build storage |
| 38 # https://wiki.corp.google.com/twiki/bin/view/Main/ChromeOfficialBuilds | |
| 39 | |
| 40 # Internal Google archive of official Chrome builds, example: | |
| 41 # https://goto.google.com/chrome_official_builds/ | |
| 42 # 32.0.1677.0/precise32bit/chrome-precise32bit.zip | |
| 43 CHROME_INTERNAL_URL_FMT = ('http://master.chrome.corp.google.com/' | |
| 44 'official_builds/%s/%s/%s') | |
|
shadi
2013/10/23 22:41:36
I was not able to bypass login requirements with g
| |
| 45 | |
| 46 # Google storage location (no public web URL's), example: | |
| 38 # gs://chrome-archive/30/30.0.1595.0/precise32bit/chrome-precise32bit.zip | 47 # gs://chrome-archive/30/30.0.1595.0/precise32bit/chrome-precise32bit.zip |
| 39 CHROME_URL_FMT = ('gs://chrome-archive/%s/%s/%s/%s') | 48 CHROME_GS_URL_FMT = ('gs://chrome-archive/%s/%s/%s/%s') |
| 40 | 49 |
| 41 | 50 |
| 42 class BuildUpdater(object): | 51 class BuildUpdater(object): |
| 43 _PLATFORM_FILES_MAP = { | 52 _PLATFORM_FILES_MAP = { |
| 44 'Win': [ | 53 'Win': [ |
| 45 'chrome-win32.zip', | 54 'chrome-win32.zip', |
| 46 'chrome-win32-syms.zip', | 55 'chrome-win32-syms.zip', |
| 47 ], | 56 ], |
| 48 'Mac': [ | 57 'Mac': [ |
| 49 'chrome-mac.zip', | 58 'chrome-mac.zip', |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 84 'Linux': 'chrome_linux', | 93 'Linux': 'chrome_linux', |
| 85 'Linux_x64': 'chrome_linux64', | 94 'Linux_x64': 'chrome_linux64', |
| 86 'Win': 'chrome_win', | 95 'Win': 'chrome_win', |
| 87 'Mac': 'chrome_mac', | 96 'Mac': 'chrome_mac', |
| 88 } | 97 } |
| 89 | 98 |
| 90 def __init__(self, options): | 99 def __init__(self, options): |
| 91 self._platforms = options.platforms.split(',') | 100 self._platforms = options.platforms.split(',') |
| 92 self._revision = options.build_number or int(options.revision) | 101 self._revision = options.build_number or int(options.revision) |
| 93 self._use_build_number = bool(options.build_number) | 102 self._use_build_number = bool(options.build_number) |
| 103 self._use_gs = options.use_gs | |
| 94 | 104 |
| 95 @staticmethod | 105 @staticmethod |
| 96 def _GetCmdStatusAndOutput(args, cwd=None, shell=False): | 106 def _GetCmdStatusAndOutput(args, cwd=None, shell=False): |
| 97 """Executes a subprocess and returns its exit code and output. | 107 """Executes a subprocess and returns its exit code and output. |
| 98 | 108 |
| 99 Args: | 109 Args: |
| 100 args: A string or a sequence of program arguments. | 110 args: A string or a sequence of program arguments. |
| 101 cwd: If not None, the subprocess's current directory will be changed to | 111 cwd: If not None, the subprocess's current directory will be changed to |
| 102 |cwd| before it's executed. | 112 |cwd| before it's executed. |
| 103 shell: Whether to execute args as a shell command. | 113 shell: Whether to execute args as a shell command. |
| 104 | 114 |
| 105 Returns: | 115 Returns: |
| 106 The tuple (exit code, output). | 116 The tuple (exit code, output). |
| 107 """ | 117 """ |
| 108 logging.info(str(args) + ' ' + (cwd or '')) | 118 logging.info(str(args) + ' ' + (cwd or '')) |
| 109 p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE, | 119 p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE, |
| 110 stderr=subprocess.PIPE, shell=shell) | 120 stderr=subprocess.PIPE, shell=shell) |
| 111 stdout, stderr = p.communicate() | 121 stdout, stderr = p.communicate() |
| 112 exit_code = p.returncode | 122 exit_code = p.returncode |
| 113 if stderr: | 123 if stderr: |
| 114 logging.critical(stderr) | 124 logging.critical(stderr) |
| 115 logging.info(stdout) | 125 logging.info(stdout) |
| 116 return (exit_code, stdout) | 126 return (exit_code, stdout) |
| 117 | 127 |
| 118 def _GetBuildUrl(self, platform, revision, filename): | 128 def _GetBuildUrl(self, platform, revision, filename): |
| 119 if self._use_build_number: | 129 if self._use_build_number: |
| 120 release = revision[:revision.find('.')] | 130 # Chrome Google storage bucket. |
| 121 return (CHROME_URL_FMT % | 131 if self._use_gs: |
| 122 (release, revision, self._BUILD_PLATFORM_MAP[platform], filename)) | 132 release = revision[:revision.find('.')] |
| 133 return (CHROME_GS_URL_FMT % ( | |
| 134 release, | |
| 135 revision, | |
| 136 self._BUILD_PLATFORM_MAP[platform], | |
| 137 filename)) | |
| 138 # Chrome internal archive. | |
| 139 return (CHROME_INTERNAL_URL_FMT % ( | |
| 140 revision, | |
| 141 self._BUILD_PLATFORM_MAP[platform], | |
| 142 filename)) | |
| 143 # Chromium archive. | |
| 123 return CHROMIUM_URL_FMT % (urllib.quote_plus(platform), revision, filename) | 144 return CHROMIUM_URL_FMT % (urllib.quote_plus(platform), revision, filename) |
| 124 | 145 |
| 125 def _FindBuildRevision(self, platform, revision, filename): | 146 def _FindBuildRevision(self, platform, revision, filename): |
| 126 # TODO(shadi): Iterate over build numbers to find a valid one. | 147 # TODO(shadi): Iterate over build numbers to find a valid one. |
| 127 if self._use_build_number: | 148 if self._use_build_number: |
| 128 return (revision | 149 return (revision |
| 129 if self._DoesChromeBuildExist(platform, revision, filename) else | 150 if self._DoesBuildExist(platform, revision, filename) else None) |
| 130 None) | |
| 131 | 151 |
| 132 MAX_REVISIONS_PER_BUILD = 100 | 152 MAX_REVISIONS_PER_BUILD = 100 |
| 133 for revision_guess in xrange(revision, revision + MAX_REVISIONS_PER_BUILD): | 153 for revision_guess in xrange(revision, revision + MAX_REVISIONS_PER_BUILD): |
| 134 if self._DoesChromiumBuildExist(platform, revision_guess, filename): | 154 if self._DoesBuildExist(platform, revision_guess, filename): |
| 135 return revision_guess | 155 return revision_guess |
| 136 else: | 156 else: |
| 137 time.sleep(.1) | 157 time.sleep(.1) |
| 138 return None | 158 return None |
| 139 | 159 |
| 140 def _DoesChromiumBuildExist(self, platform, build_number, filename): | 160 def _DoesBuildExist(self, platform, build_number, filename): |
| 141 url = self._GetBuildUrl(platform, build_number, filename) | 161 url = self._GetBuildUrl(platform, build_number, filename) |
| 162 if self._use_gs: | |
| 163 return self._DoesGSFileExist(url) | |
| 164 | |
| 142 r = urllib2.Request(url) | 165 r = urllib2.Request(url) |
| 143 r.get_method = lambda: 'HEAD' | 166 r.get_method = lambda: 'HEAD' |
| 144 try: | 167 try: |
| 145 urllib2.urlopen(r) | 168 urllib2.urlopen(r) |
| 146 return True | 169 return True |
| 147 except urllib2.HTTPError, err: | 170 except urllib2.HTTPError, err: |
| 148 if err.code == 404: | 171 if err.code == 404: |
| 149 return False | 172 return False |
| 150 | 173 |
| 151 def _DoesChromeBuildExist(self, platform, build_number, filename): | 174 def _DoesGSFileExist(self, gs_file_name): |
| 152 release = build_number[:build_number.find('.')] | 175 exit_code = BuildUpdater._GetCmdStatusAndOutput( |
| 153 gs_file = (CHROME_URL_FMT % | 176 ['gsutil', 'ls', gs_file_name])[0] |
| 154 (release, | |
| 155 build_number, | |
| 156 self._BUILD_PLATFORM_MAP[platform], | |
| 157 filename)) | |
| 158 (exit_code, stdout) = BuildUpdater._GetCmdStatusAndOutput( | |
| 159 ['gsutil', 'ls', gs_file]) | |
| 160 | |
| 161 return not exit_code | 177 return not exit_code |
| 162 | 178 |
| 163 def _GetPlatformFiles(self, platform): | 179 def _GetPlatformFiles(self, platform): |
| 164 if self._use_build_number: | 180 if self._use_build_number: |
| 165 return BuildUpdater._CHROME_PLATFORM_FILES_MAP[platform] | 181 return BuildUpdater._CHROME_PLATFORM_FILES_MAP[platform] |
| 166 return BuildUpdater._PLATFORM_FILES_MAP[platform] | 182 return BuildUpdater._PLATFORM_FILES_MAP[platform] |
| 167 | 183 |
| 168 def _DownloadBuilds(self): | 184 def _DownloadBuilds(self): |
| 169 for platform in self._platforms: | 185 for platform in self._platforms: |
| 170 for f in self._GetPlatformFiles(platform): | 186 for f in self._GetPlatformFiles(platform): |
| 171 output = os.path.join('dl', platform, | 187 output = os.path.join('dl', platform, |
| 172 '%s_%s_%s' % (platform, self._revision, f)) | 188 '%s_%s_%s' % (platform, self._revision, f)) |
| 173 if os.path.exists(output): | 189 if os.path.exists(output): |
| 174 logging.info('%s alread exists, skipping download' % output) | 190 logging.info('%s alread exists, skipping download', output) |
| 175 continue | 191 continue |
| 176 build_revision = self._FindBuildRevision(platform, self._revision, f) | 192 build_revision = self._FindBuildRevision(platform, self._revision, f) |
| 177 if not build_revision: | 193 if not build_revision: |
| 178 logging.critical('Failed to find %s build for r%s\n' % ( | 194 logging.critical('Failed to find %s build for r%s\n', platform, |
| 179 platform, self._revision)) | 195 self._revision) |
| 180 sys.exit(1) | 196 sys.exit(1) |
| 181 dirname = os.path.dirname(output) | 197 dirname = os.path.dirname(output) |
| 182 if dirname and not os.path.exists(dirname): | 198 if dirname and not os.path.exists(dirname): |
| 183 os.makedirs(dirname) | 199 os.makedirs(dirname) |
| 184 url = self._GetBuildUrl(platform, build_revision, f) | 200 url = self._GetBuildUrl(platform, build_revision, f) |
| 185 self._DownloadFile(url, output) | 201 self._DownloadFile(url, output) |
| 186 | 202 |
| 187 def _DownloadFile(self, url, output): | 203 def _DownloadFile(self, url, output): |
| 188 logging.info('Downloading %s, saving to %s' % (url, output)) | 204 logging.info('Downloading %s, saving to %s', url, output) |
| 189 if self._use_build_number: | 205 if self._use_build_number and self._use_gs: |
| 190 BuildUpdater._GetCmdStatusAndOutput(['gsutil', 'cp', url, output]) | 206 BuildUpdater._GetCmdStatusAndOutput(['gsutil', 'cp', url, output]) |
| 191 else: | 207 else: |
| 192 r = urllib2.urlopen(url) | 208 r = urllib2.urlopen(url) |
| 193 with file(output, 'wb') as f: | 209 with file(output, 'wb') as f: |
| 194 f.write(r.read()) | 210 f.write(r.read()) |
| 195 | 211 |
| 196 def _FetchSvnRepos(self): | 212 def _FetchSvnRepos(self): |
| 197 if not os.path.exists('reference_builds'): | 213 if not os.path.exists('reference_builds'): |
| 198 os.makedirs('reference_builds') | 214 os.makedirs('reference_builds') |
| 199 BuildUpdater._GetCmdStatusAndOutput( | 215 BuildUpdater._GetCmdStatusAndOutput( |
| 200 ['gclient', 'config', | 216 ['gclient', 'config', |
| 201 'svn://svn.chromium.org/chrome/trunk/deps/reference_builds'], | 217 'svn://svn.chromium.org/chrome/trunk/deps/reference_builds'], |
| 202 'reference_builds') | 218 'reference_builds') |
| 203 BuildUpdater._GetCmdStatusAndOutput( | 219 BuildUpdater._GetCmdStatusAndOutput( |
| 204 ['gclient', 'sync'], 'reference_builds') | 220 ['gclient', 'sync'], 'reference_builds') |
| 205 | 221 |
| 206 def _UnzipFile(self, dl_file, dest_dir): | 222 def _UnzipFile(self, dl_file, dest_dir): |
| 207 if not zipfile.is_zipfile(dl_file): | 223 if not zipfile.is_zipfile(dl_file): |
| 208 return False | 224 return False |
| 209 logging.info('Opening %s' % dl_file) | 225 logging.info('Opening %s', dl_file) |
| 210 with zipfile.ZipFile(dl_file, 'r') as z: | 226 with zipfile.ZipFile(dl_file, 'r') as z: |
| 211 for content in z.namelist(): | 227 for content in z.namelist(): |
| 212 dest = os.path.join(dest_dir, content[content.find('/')+1:]) | 228 dest = os.path.join(dest_dir, content[content.find('/')+1:]) |
| 213 # Create dest parent dir if it does not exist. | 229 # Create dest parent dir if it does not exist. |
| 214 if not os.path.isdir(os.path.dirname(dest)): | 230 if not os.path.isdir(os.path.dirname(dest)): |
| 215 os.makedirs(os.path.dirname(dest)) | 231 os.makedirs(os.path.dirname(dest)) |
| 216 # If dest is just a dir listing, do nothing. | 232 # If dest is just a dir listing, do nothing. |
| 217 if not os.path.basename(dest): | 233 if not os.path.basename(dest): |
| 218 continue | 234 continue |
| 219 with z.open(content) as unzipped_content: | 235 with z.open(content) as unzipped_content: |
| 220 logging.info('Extracting %s to %s (%s)' % (content, dest, dl_file)) | 236 logging.info('Extracting %s to %s (%s)', content, dest, dl_file) |
| 221 with file(dest, 'wb') as dest_file: | 237 with file(dest, 'wb') as dest_file: |
| 222 dest_file.write(unzipped_content.read()) | 238 dest_file.write(unzipped_content.read()) |
| 223 permissions = z.getinfo(content).external_attr >> 16 | 239 permissions = z.getinfo(content).external_attr >> 16 |
| 224 if permissions: | 240 if permissions: |
| 225 os.chmod(dest, permissions) | 241 os.chmod(dest, permissions) |
| 226 return True | 242 return True |
| 227 | 243 |
| 228 def _ClearDir(self, dir): | 244 def _ClearDir(self, dir): |
| 229 """Clears all files in |dir| except for hidden files and folders.""" | 245 """Clears all files in |dir| except for hidden files and folders.""" |
| 230 for root, dirs, files in os.walk(dir): | 246 for root, dirs, files in os.walk(dir): |
| 231 # Skip hidden files and folders (like .svn and .git). | 247 # Skip hidden files and folders (like .svn and .git). |
| 232 files = [f for f in files if f[0] != '.'] | 248 files = [f for f in files if f[0] != '.'] |
| 233 dirs[:] = [d for d in dirs if d[0] != '.'] | 249 dirs[:] = [d for d in dirs if d[0] != '.'] |
| 234 | 250 |
| 235 for f in files: | 251 for f in files: |
| 236 os.remove(os.path.join(root, f)) | 252 os.remove(os.path.join(root, f)) |
| 237 | 253 |
| 238 def _ExtractBuilds(self): | 254 def _ExtractBuilds(self): |
| 239 for platform in self._platforms: | 255 for platform in self._platforms: |
| 240 if os.path.exists('tmp_unzip'): | 256 if os.path.exists('tmp_unzip'): |
| 241 os.path.unlink('tmp_unzip') | 257 os.path.unlink('tmp_unzip') |
| 242 dest_dir = os.path.join('reference_builds', 'reference_builds', | 258 dest_dir = os.path.join('reference_builds', 'reference_builds', |
| 243 BuildUpdater._PLATFORM_DEST_MAP[platform]) | 259 BuildUpdater._PLATFORM_DEST_MAP[platform]) |
| 244 self._ClearDir(dest_dir) | 260 self._ClearDir(dest_dir) |
| 245 for root, _, dl_files in os.walk(os.path.join('dl', platform)): | 261 for root, _, dl_files in os.walk(os.path.join('dl', platform)): |
| 246 for dl_file in dl_files: | 262 for dl_file in dl_files: |
| 247 dl_file = os.path.join(root, dl_file) | 263 dl_file = os.path.join(root, dl_file) |
| 248 if not self._UnzipFile(dl_file, dest_dir): | 264 if not self._UnzipFile(dl_file, dest_dir): |
| 249 logging.info('Copying %s to %s' % (dl_file, dest_dir)) | 265 logging.info('Copying %s to %s', dl_file, dest_dir) |
| 250 shutil.copy(dl_file, dest_dir) | 266 shutil.copy(dl_file, dest_dir) |
| 251 | 267 |
| 252 def _SvnAddAndRemove(self): | 268 def _SvnAddAndRemove(self): |
| 253 svn_dir = os.path.join('reference_builds', 'reference_builds') | 269 svn_dir = os.path.join('reference_builds', 'reference_builds') |
| 254 stat = BuildUpdater._GetCmdStatusAndOutput(['svn', 'stat'], svn_dir)[1] | 270 # List all changes without ignoring any files. |
| 271 stat = BuildUpdater._GetCmdStatusAndOutput(['svn', 'stat', '--no-ignore'], | |
| 272 svn_dir)[1] | |
| 255 for line in stat.splitlines(): | 273 for line in stat.splitlines(): |
| 256 action, filename = line.split(None, 1) | 274 action, filename = line.split(None, 1) |
| 257 if action == '?': | 275 # Add new and ignored files. |
| 276 if action == '?' or action == 'I': | |
| 258 BuildUpdater._GetCmdStatusAndOutput( | 277 BuildUpdater._GetCmdStatusAndOutput( |
| 259 ['svn', 'add', filename], svn_dir) | 278 ['svn', 'add', filename], svn_dir) |
| 260 elif action == '!': | 279 elif action == '!': |
| 261 BuildUpdater._GetCmdStatusAndOutput( | 280 BuildUpdater._GetCmdStatusAndOutput( |
| 262 ['svn', 'delete', filename], svn_dir) | 281 ['svn', 'delete', filename], svn_dir) |
| 263 filepath = os.path.join(svn_dir, filename) | 282 filepath = os.path.join(svn_dir, filename) |
| 264 if not os.path.isdir(filepath) and os.access(filepath, os.X_OK): | 283 if not os.path.isdir(filepath) and os.access(filepath, os.X_OK): |
| 265 BuildUpdater._GetCmdStatusAndOutput( | 284 BuildUpdater._GetCmdStatusAndOutput( |
| 266 ['svn', 'propset', 'svn:executable', 'true', filename], svn_dir) | 285 ['svn', 'propset', 'svn:executable', 'true', filename], svn_dir) |
| 267 | 286 |
| 268 def DownloadAndUpdateBuilds(self): | 287 def DownloadAndUpdateBuilds(self): |
| 269 self._DownloadBuilds() | 288 self._DownloadBuilds() |
| 270 self._FetchSvnRepos() | 289 self._FetchSvnRepos() |
| 271 self._ExtractBuilds() | 290 self._ExtractBuilds() |
| 272 self._SvnAddAndRemove() | 291 self._SvnAddAndRemove() |
| 273 | 292 |
| 274 | 293 |
| 275 def ParseOptions(argv): | 294 def ParseOptions(argv): |
| 276 parser = optparse.OptionParser() | 295 parser = optparse.OptionParser() |
| 277 usage = 'usage: %prog <options>' | 296 usage = 'usage: %prog <options>' |
| 278 parser.set_usage(usage) | 297 parser.set_usage(usage) |
| 279 parser.add_option('-b', dest='build_number', | 298 parser.add_option('-b', dest='build_number', |
| 280 help='Chrome official build number to pick up.') | 299 help='Chrome official build number to pick up.') |
| 281 parser.add_option('-r', dest='revision', | 300 parser.add_option('--gs', dest='use_gs', action='store_true', default=False, |
| 282 help='Revision to pick up.') | 301 help='Use Google storage for official builds. Used with -b ' |
| 302 'option. Default is false (i.e. use internal storage.') | |
| 283 parser.add_option('-p', dest='platforms', | 303 parser.add_option('-p', dest='platforms', |
| 284 default='Win,Mac,Linux,Linux_x64', | 304 default='Win,Mac,Linux,Linux_x64', |
| 285 help='Comma separated list of platforms to download ' | 305 help='Comma separated list of platforms to download ' |
| 286 '(as defined by the chromium builders).') | 306 '(as defined by the chromium builders).') |
| 307 parser.add_option('-r', dest='revision', | |
| 308 help='Revision to pick up.') | |
| 309 | |
| 287 (options, _) = parser.parse_args(argv) | 310 (options, _) = parser.parse_args(argv) |
| 288 if not options.revision and not options.build_number: | 311 if not options.revision and not options.build_number: |
| 289 logging.critical('Must specify either -r or -b.\n') | 312 logging.critical('Must specify either -r or -b.\n') |
| 290 sys.exit(1) | 313 sys.exit(1) |
| 291 if options.revision and options.build_number: | 314 if options.revision and options.build_number: |
| 292 logging.critical('Must specify either -r or -b but not both.\n') | 315 logging.critical('Must specify either -r or -b but not both.\n') |
| 293 sys.exit(1) | 316 sys.exit(1) |
| 317 if options.use_gs and not options.build_number: | |
| 318 logging.critical('Can only use --gs with -b option.\n') | |
| 319 sys.exit(1) | |
| 294 | 320 |
| 295 return options | 321 return options |
| 296 | 322 |
| 297 | 323 |
| 298 def main(argv): | 324 def main(argv): |
| 299 logging.getLogger().setLevel(logging.DEBUG) | 325 logging.getLogger().setLevel(logging.DEBUG) |
| 300 options = ParseOptions(argv) | 326 options = ParseOptions(argv) |
| 301 b = BuildUpdater(options) | 327 b = BuildUpdater(options) |
| 302 b.DownloadAndUpdateBuilds() | 328 b.DownloadAndUpdateBuilds() |
| 303 logging.info('Successfully updated reference builds. Move to ' | 329 logging.info('Successfully updated reference builds. Move to ' |
| 304 'reference_builds/reference_builds and make a change with gcl.') | 330 'reference_builds/reference_builds and make a change with gcl.') |
| 305 | 331 |
| 306 if __name__ == '__main__': | 332 if __name__ == '__main__': |
| 307 sys.exit(main(sys.argv)) | 333 sys.exit(main(sys.argv)) |
| OLD | NEW |