codesign.py (24589B)
1 # Copyright 2016 The Chromium Authors 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 6 import argparse 7 import codecs 8 import datetime 9 import fnmatch 10 import glob 11 import json 12 import os 13 import plistlib 14 import shutil 15 import subprocess 16 import stat 17 import sys 18 import tempfile 19 20 # Keys that should not be copied from mobileprovision 21 BANNED_KEYS = [ 22 "com.apple.developer.cs.allow-jit", 23 "com.apple.developer.memory.transfer-send", 24 "com.apple.developer.web-browser", 25 "com.apple.developer.web-browser-engine.host", 26 "com.apple.developer.web-browser-engine.networking", 27 "com.apple.developer.web-browser-engine.rendering", 28 "com.apple.developer.web-browser-engine.webcontent", 29 ] 30 31 if sys.version_info.major < 3: 32 basestring_compat = basestring 33 else: 34 basestring_compat = str 35 36 37 def GetProvisioningProfilesDir(): 38 """Returns the location of the installed mobile provisioning profiles. 39 40 Returns: 41 The path to the directory containing the installed mobile provisioning 42 profiles as a string. 43 """ 44 return os.path.join( 45 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') 46 47 48 def ReadPlistFromString(plist_bytes): 49 """Parse property list from given |plist_bytes|. 50 51 Args: 52 plist_bytes: contents of property list to load. Must be bytes in python 3. 53 54 Returns: 55 The contents of property list as a python object. 56 """ 57 if sys.version_info.major == 2: 58 return plistlib.readPlistFromString(plist_bytes) 59 else: 60 return plistlib.loads(plist_bytes) 61 62 63 def LoadPlistFile(plist_path): 64 """Loads property list file at |plist_path|. 65 66 Args: 67 plist_path: path to the property list file to load. 68 69 Returns: 70 The content of the property list file as a python object. 71 """ 72 if sys.version_info.major == 2: 73 return plistlib.readPlistFromString( 74 subprocess.check_output( 75 ['xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path])) 76 else: 77 with open(plist_path, 'rb') as fp: 78 return plistlib.load(fp) 79 80 81 def CreateSymlink(value, location): 82 """Creates symlink with value at location if the target exists.""" 83 target = os.path.join(os.path.dirname(location), value) 84 if os.path.exists(location): 85 os.unlink(location) 86 os.symlink(value, location) 87 88 89 class Bundle(object): 90 """Wraps a bundle.""" 91 92 def __init__(self, bundle_path, platform): 93 """Initializes the Bundle object with data from bundle Info.plist file.""" 94 self._path = bundle_path 95 self._kind = Bundle.Kind(platform, os.path.splitext(bundle_path)[-1]) 96 self._data = None 97 98 def Load(self): 99 self._data = LoadPlistFile(self.info_plist_path) 100 101 @staticmethod 102 def Kind(platform, extension): 103 if platform in ('iphoneos', 'iphonesimulator'): 104 return 'ios' 105 if platform == 'macosx': 106 if extension == '.framework': 107 return 'mac_framework' 108 return 'mac' 109 if platform in ('watchos', 'watchsimulator'): 110 return 'watchos' 111 if platform in ('appletvos', 'appletvsimulator'): 112 return 'tvos' 113 raise ValueError('unknown bundle type %s for %s' % (extension, platform)) 114 115 @property 116 def kind(self): 117 return self._kind 118 119 @property 120 def path(self): 121 return self._path 122 123 @property 124 def contents_dir(self): 125 if self._kind == 'mac': 126 return os.path.join(self.path, 'Contents') 127 if self._kind == 'mac_framework': 128 return os.path.join(self.path, 'Versions/A') 129 return self.path 130 131 @property 132 def executable_dir(self): 133 if self._kind == 'mac': 134 return os.path.join(self.contents_dir, 'MacOS') 135 return self.contents_dir 136 137 @property 138 def resources_dir(self): 139 if self._kind == 'mac' or self._kind == 'mac_framework': 140 return os.path.join(self.contents_dir, 'Resources') 141 return self.path 142 143 @property 144 def info_plist_path(self): 145 if self._kind == 'mac_framework': 146 return os.path.join(self.resources_dir, 'Info.plist') 147 return os.path.join(self.contents_dir, 'Info.plist') 148 149 @property 150 def signature_dir(self): 151 return os.path.join(self.contents_dir, '_CodeSignature') 152 153 @property 154 def identifier(self): 155 return self._data['CFBundleIdentifier'] 156 157 @property 158 def binary_name(self): 159 return self._data['CFBundleExecutable'] 160 161 @property 162 def binary_path(self): 163 return os.path.join(self.executable_dir, self.binary_name) 164 165 def Validate(self, expected_mappings): 166 """Checks that keys in the bundle have the expected value. 167 168 Args: 169 expected_mappings: a dictionary of string to object, each mapping will 170 be looked up in the bundle data to check it has the same value (missing 171 values will be ignored) 172 173 Returns: 174 A dictionary of the key with a different value between expected_mappings 175 and the content of the bundle (i.e. errors) so that caller can format the 176 error message. The dictionary will be empty if there are no errors. 177 """ 178 errors = {} 179 for key, expected_value in expected_mappings.items(): 180 if key in self._data: 181 value = self._data[key] 182 if value != expected_value: 183 errors[key] = (value, expected_value) 184 return errors 185 186 187 class ProvisioningProfile(object): 188 """Wraps a mobile provisioning profile file.""" 189 190 def __init__(self, provisioning_profile_path): 191 """Initializes the ProvisioningProfile with data from profile file.""" 192 self._path = provisioning_profile_path 193 self._data = ReadPlistFromString( 194 subprocess.check_output([ 195 'xcrun', 'security', 'cms', '-D', '-u', 'certUsageAnyCA', '-i', 196 provisioning_profile_path 197 ])) 198 199 @property 200 def path(self): 201 return self._path 202 203 @property 204 def team_identifier(self): 205 return self._data.get('TeamIdentifier', [''])[0] 206 207 @property 208 def name(self): 209 return self._data.get('Name', '') 210 211 @property 212 def application_identifier_pattern(self): 213 return self._data.get('Entitlements', {}).get('application-identifier', '') 214 215 @property 216 def application_identifier_prefix(self): 217 return self._data.get('ApplicationIdentifierPrefix', [''])[0] 218 219 @property 220 def entitlements(self): 221 return self._data.get('Entitlements', {}) 222 223 @property 224 def expiration_date(self): 225 return self._data.get('ExpirationDate', datetime.datetime.now()) 226 227 def ValidToSignBundle(self, bundle_identifier): 228 """Checks whether the provisioning profile can sign bundle_identifier. 229 230 Args: 231 bundle_identifier: the identifier of the bundle that needs to be signed. 232 233 Returns: 234 True if the mobile provisioning profile can be used to sign a bundle 235 with the corresponding bundle_identifier, False otherwise. 236 """ 237 return fnmatch.fnmatch( 238 '%s.%s' % (self.application_identifier_prefix, bundle_identifier), 239 self.application_identifier_pattern) 240 241 def Install(self, installation_path): 242 """Copies mobile provisioning profile info to |installation_path|.""" 243 shutil.copy2(self.path, installation_path) 244 st = os.stat(installation_path) 245 os.chmod(installation_path, st.st_mode | stat.S_IWUSR) 246 247 248 class Entitlements(object): 249 """Wraps an Entitlement plist file.""" 250 251 def __init__(self, entitlements_path): 252 """Initializes Entitlements object from entitlement file.""" 253 self._path = entitlements_path 254 self._data = LoadPlistFile(self._path) 255 256 @property 257 def path(self): 258 return self._path 259 260 def ExpandVariables(self, substitutions): 261 self._data = self._ExpandVariables(self._data, substitutions) 262 263 def _ExpandVariables(self, data, substitutions): 264 if isinstance(data, basestring_compat): 265 for key, substitution in substitutions.items(): 266 data = data.replace('$(%s)' % (key,), substitution) 267 return data 268 269 if isinstance(data, dict): 270 for key, value in data.items(): 271 data[key] = self._ExpandVariables(value, substitutions) 272 return data 273 274 if isinstance(data, list): 275 for i, value in enumerate(data): 276 data[i] = self._ExpandVariables(value, substitutions) 277 278 return data 279 280 def LoadDefaults(self, defaults): 281 for key, value in defaults.items(): 282 if key not in self._data and key not in BANNED_KEYS: 283 self._data[key] = value 284 285 def WriteTo(self, target_path): 286 with open(target_path, 'wb') as fp: 287 if sys.version_info.major == 2: 288 plistlib.writePlist(self._data, fp) 289 else: 290 plistlib.dump(self._data, fp) 291 292 293 def FindProvisioningProfile(provisioning_profile_paths, bundle_identifier, 294 required): 295 """Finds mobile provisioning profile to use to sign bundle. 296 297 Args: 298 bundle_identifier: the identifier of the bundle to sign. 299 300 Returns: 301 The ProvisioningProfile object that can be used to sign the Bundle 302 object or None if no matching provisioning profile was found. 303 """ 304 if not provisioning_profile_paths: 305 provisioning_profile_paths = glob.glob( 306 os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision')) 307 308 # Iterate over all installed mobile provisioning profiles and filter those 309 # that can be used to sign the bundle, ignoring expired ones. 310 now = datetime.datetime.now() 311 valid_provisioning_profiles = [] 312 one_hour = datetime.timedelta(0, 3600) 313 for provisioning_profile_path in provisioning_profile_paths: 314 provisioning_profile = ProvisioningProfile(provisioning_profile_path) 315 if provisioning_profile.expiration_date - now < one_hour: 316 sys.stderr.write( 317 'Warning: ignoring expired provisioning profile: %s.\n' % 318 provisioning_profile_path) 319 continue 320 if provisioning_profile.ValidToSignBundle(bundle_identifier): 321 valid_provisioning_profiles.append(provisioning_profile) 322 323 if not valid_provisioning_profiles: 324 if required: 325 sys.stderr.write( 326 'Error: no mobile provisioning profile found for "%s" in %s.\n' % 327 (bundle_identifier, provisioning_profile_paths)) 328 sys.exit(1) 329 return None 330 331 # Select the most specific mobile provisioning profile, i.e. the one with 332 # the longest application identifier pattern (prefer the one with the latest 333 # expiration date as a secondary criteria). 334 selected_provisioning_profile = max( 335 valid_provisioning_profiles, 336 key=lambda p: (len(p.application_identifier_pattern), p.expiration_date)) 337 338 one_week = datetime.timedelta(7) 339 if selected_provisioning_profile.expiration_date - now < 2 * one_week: 340 sys.stderr.write( 341 'Warning: selected provisioning profile will expire soon: %s' % 342 selected_provisioning_profile.path) 343 return selected_provisioning_profile 344 345 346 def CodeSignBundle(bundle_path, identity, extra_args): 347 process = subprocess.Popen( 348 ['xcrun', 'codesign', '--force', '--sign', identity, '--timestamp=none'] + 349 list(extra_args) + [bundle_path], 350 stderr=subprocess.PIPE, 351 universal_newlines=True) 352 _, stderr = process.communicate() 353 if process.returncode: 354 sys.stderr.write(stderr) 355 sys.exit(process.returncode) 356 for line in stderr.splitlines(): 357 if line.endswith(': replacing existing signature'): 358 # Ignore warning about replacing existing signature as this should only 359 # happen when re-signing system frameworks (and then it is expected). 360 continue 361 sys.stderr.write(line) 362 sys.stderr.write('\n') 363 364 365 def InstallSystemFramework(framework_path, bundle_path, args): 366 """Install framework from |framework_path| to |bundle| and code-re-sign it.""" 367 installed_framework_path = os.path.join( 368 bundle_path, 'Frameworks', os.path.basename(framework_path)) 369 370 if os.path.isfile(framework_path): 371 shutil.copy(framework_path, installed_framework_path) 372 elif os.path.isdir(framework_path): 373 if os.path.exists(installed_framework_path): 374 shutil.rmtree(installed_framework_path) 375 shutil.copytree(framework_path, installed_framework_path) 376 377 CodeSignBundle(installed_framework_path, args.identity, 378 ['--deep', '--preserve-metadata=identifier,entitlements,flags']) 379 380 381 def GenerateEntitlements(path, provisioning_profile, bundle_identifier): 382 """Generates an entitlements file. 383 384 Args: 385 path: path to the entitlements template file 386 provisioning_profile: ProvisioningProfile object to use, may be None 387 bundle_identifier: identifier of the bundle to sign. 388 """ 389 entitlements = Entitlements(path) 390 if provisioning_profile: 391 entitlements.LoadDefaults(provisioning_profile.entitlements) 392 app_identifier_prefix = \ 393 provisioning_profile.application_identifier_prefix + '.' 394 else: 395 app_identifier_prefix = '*.' 396 entitlements.ExpandVariables({ 397 'CFBundleIdentifier': bundle_identifier, 398 'AppIdentifierPrefix': app_identifier_prefix, 399 }) 400 return entitlements 401 402 403 def GenerateBundleInfoPlist(bundle, plist_compiler, partial_plist): 404 """Generates the bundle Info.plist for a list of partial .plist files. 405 406 Args: 407 bundle: a Bundle instance 408 plist_compiler: string, path to the Info.plist compiler 409 partial_plist: list of path to partial .plist files to merge 410 """ 411 412 # Filter empty partial .plist files (this happens if an application 413 # does not compile any asset catalog, in which case the partial .plist 414 # file from the asset catalog compilation step is just a stamp file). 415 filtered_partial_plist = [] 416 for plist in partial_plist: 417 plist_size = os.stat(plist).st_size 418 if plist_size: 419 filtered_partial_plist.append(plist) 420 421 # Invoke the plist_compiler script. It needs to be a python script. 422 subprocess.check_call([ 423 'python3', 424 plist_compiler, 425 'merge', 426 '-f', 427 'binary1', 428 '-o', 429 bundle.info_plist_path, 430 ] + filtered_partial_plist) 431 432 433 class Action(object): 434 """Class implementing one action supported by the script.""" 435 436 @classmethod 437 def Register(cls, subparsers): 438 parser = subparsers.add_parser(cls.name, help=cls.help) 439 parser.set_defaults(func=cls._Execute) 440 cls._Register(parser) 441 442 443 class CodeSignBundleAction(Action): 444 """Class implementing the code-sign-bundle action.""" 445 446 name = 'code-sign-bundle' 447 help = 'perform code signature for a bundle' 448 449 @staticmethod 450 def _Register(parser): 451 parser.add_argument( 452 '--entitlements', '-e', dest='entitlements_path', 453 help='path to the entitlements file to use') 454 parser.add_argument( 455 'path', help='path to the iOS bundle to codesign') 456 parser.add_argument( 457 '--identity', '-i', required=True, 458 help='identity to use to codesign') 459 parser.add_argument( 460 '--binary', '-b', required=True, 461 help='path to the iOS bundle binary') 462 parser.add_argument( 463 '--framework', '-F', action='append', default=[], dest='frameworks', 464 help='install and resign system framework') 465 parser.add_argument( 466 '--disable-code-signature', action='store_true', dest='no_signature', 467 help='disable code signature') 468 parser.add_argument( 469 '--disable-embedded-mobileprovision', action='store_false', 470 default=True, dest='embedded_mobileprovision', 471 help='disable finding and embedding mobileprovision') 472 parser.add_argument( 473 '--platform', '-t', required=True, 474 help='platform the signed bundle is targeting') 475 parser.add_argument( 476 '--partial-info-plist', '-p', action='append', default=[], 477 help='path to partial Info.plist to merge to create bundle Info.plist') 478 parser.add_argument( 479 '--plist-compiler-path', '-P', action='store', 480 help='path to the plist compiler script (for --partial-info-plist)') 481 parser.add_argument( 482 '--mobileprovision', 483 '-m', 484 action='append', 485 default=[], 486 dest='mobileprovision_files', 487 help='list of mobileprovision files to use. If empty, uses the files ' + 488 'in $HOME/Library/MobileDevice/Provisioning Profiles') 489 parser.set_defaults(no_signature=False) 490 491 @staticmethod 492 def _Execute(args): 493 if not args.identity: 494 args.identity = '-' 495 496 bundle = Bundle(args.path, args.platform) 497 498 if args.partial_info_plist: 499 GenerateBundleInfoPlist(bundle, args.plist_compiler_path, 500 args.partial_info_plist) 501 502 # The bundle Info.plist may have been updated by GenerateBundleInfoPlist() 503 # above. Load the bundle information from Info.plist after the modification 504 # have been written to disk. 505 bundle.Load() 506 507 # According to Apple documentation, the application binary must be the same 508 # as the bundle name without the .app suffix. See crbug.com/740476 for more 509 # information on what problem this can cause. 510 # 511 # To prevent this class of error, fail with an error if the binary name is 512 # incorrect in the Info.plist as it is not possible to update the value in 513 # Info.plist at this point (the file has been copied by a different target 514 # and ninja would consider the build dirty if it was updated). 515 # 516 # Also checks that the name of the bundle is correct too (does not cause the 517 # build to be considered dirty, but still terminate the script in case of an 518 # incorrect bundle name). 519 # 520 # Apple documentation is available at: 521 # https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html 522 bundle_name = os.path.splitext(os.path.basename(bundle.path))[0] 523 errors = bundle.Validate({ 524 'CFBundleName': bundle_name, 525 'CFBundleExecutable': bundle_name, 526 }) 527 if errors: 528 for key in sorted(errors): 529 value, expected_value = errors[key] 530 sys.stderr.write('%s: error: %s value incorrect: %s != %s\n' % ( 531 bundle.path, key, value, expected_value)) 532 sys.stderr.flush() 533 sys.exit(1) 534 535 # Delete existing embedded mobile provisioning. 536 embedded_provisioning_profile = os.path.join( 537 bundle.path, 'embedded.mobileprovision') 538 if os.path.isfile(embedded_provisioning_profile): 539 os.unlink(embedded_provisioning_profile) 540 541 # Delete existing code signature. 542 if os.path.exists(bundle.signature_dir): 543 shutil.rmtree(bundle.signature_dir) 544 545 # Install system frameworks if requested. 546 for framework_path in args.frameworks: 547 InstallSystemFramework(framework_path, args.path, args) 548 549 # Copy main binary into bundle. 550 if not os.path.isdir(bundle.executable_dir): 551 os.makedirs(bundle.executable_dir) 552 shutil.copy(args.binary, bundle.binary_path) 553 554 if bundle.kind == 'mac_framework': 555 # Create Versions/Current -> Versions/A symlink 556 CreateSymlink('A', os.path.join(bundle.path, 'Versions/Current')) 557 558 # Create $binary_name -> Versions/Current/$binary_name symlink 559 CreateSymlink(os.path.join('Versions/Current', bundle.binary_name), 560 os.path.join(bundle.path, bundle.binary_name)) 561 562 # Create optional symlinks. 563 for name in ('Headers', 'Resources', 'Modules'): 564 target = os.path.join(bundle.path, 'Versions/A', name) 565 if os.path.exists(target): 566 CreateSymlink(os.path.join('Versions/Current', name), 567 os.path.join(bundle.path, name)) 568 else: 569 obsolete_path = os.path.join(bundle.path, name) 570 if os.path.exists(obsolete_path): 571 os.unlink(obsolete_path) 572 573 if args.no_signature: 574 return 575 576 codesign_extra_args = [] 577 578 if args.embedded_mobileprovision: 579 # Find mobile provisioning profile and embeds it into the bundle (if a 580 # code signing identify has been provided, fails if no valid mobile 581 # provisioning is found). 582 provisioning_profile_required = args.identity != '-' 583 provisioning_profile = FindProvisioningProfile( 584 args.mobileprovision_files, bundle.identifier, 585 provisioning_profile_required) 586 if provisioning_profile and not args.platform.endswith('simulator'): 587 provisioning_profile.Install(embedded_provisioning_profile) 588 589 if args.entitlements_path is not None: 590 temporary_entitlements_file = \ 591 tempfile.NamedTemporaryFile(suffix='.xcent') 592 codesign_extra_args.extend( 593 ['--entitlements', temporary_entitlements_file.name]) 594 595 entitlements = GenerateEntitlements( 596 args.entitlements_path, provisioning_profile, bundle.identifier) 597 entitlements.WriteTo(temporary_entitlements_file.name) 598 599 CodeSignBundle(bundle.path, args.identity, codesign_extra_args) 600 601 602 class CodeSignFileAction(Action): 603 """Class implementing code signature for a single file.""" 604 605 name = 'code-sign-file' 606 help = 'code-sign a single file' 607 608 @staticmethod 609 def _Register(parser): 610 parser.add_argument( 611 'path', help='path to the file to codesign') 612 parser.add_argument( 613 '--identity', '-i', required=True, 614 help='identity to use to codesign') 615 parser.add_argument( 616 '--output', '-o', 617 help='if specified copy the file to that location before signing it') 618 parser.set_defaults(sign=True) 619 620 @staticmethod 621 def _Execute(args): 622 if not args.identity: 623 args.identity = '-' 624 625 install_path = args.path 626 if args.output: 627 628 if os.path.isfile(args.output): 629 os.unlink(args.output) 630 elif os.path.isdir(args.output): 631 shutil.rmtree(args.output) 632 633 if os.path.isfile(args.path): 634 shutil.copy(args.path, args.output) 635 elif os.path.isdir(args.path): 636 shutil.copytree(args.path, args.output) 637 638 install_path = args.output 639 640 CodeSignBundle(install_path, args.identity, 641 ['--deep', '--preserve-metadata=identifier,entitlements']) 642 643 644 class GenerateEntitlementsAction(Action): 645 """Class implementing the generate-entitlements action.""" 646 647 name = 'generate-entitlements' 648 help = 'generate entitlements file' 649 650 @staticmethod 651 def _Register(parser): 652 parser.add_argument( 653 '--entitlements', '-e', dest='entitlements_path', 654 help='path to the entitlements file to use') 655 parser.add_argument( 656 'path', help='path to the entitlements file to generate') 657 parser.add_argument( 658 '--info-plist', '-p', required=True, 659 help='path to the bundle Info.plist') 660 parser.add_argument( 661 '--mobileprovision', 662 '-m', 663 action='append', 664 default=[], 665 dest='mobileprovision_files', 666 help='set of mobileprovision files to use. If empty, uses the files ' + 667 'in $HOME/Library/MobileDevice/Provisioning Profiles') 668 669 @staticmethod 670 def _Execute(args): 671 info_plist = LoadPlistFile(args.info_plist) 672 bundle_identifier = info_plist['CFBundleIdentifier'] 673 provisioning_profile = FindProvisioningProfile(args.mobileprovision_files, 674 bundle_identifier, False) 675 entitlements = GenerateEntitlements( 676 args.entitlements_path, provisioning_profile, bundle_identifier) 677 entitlements.WriteTo(args.path) 678 679 680 class FindProvisioningProfileAction(Action): 681 """Class implementing the find-codesign-identity action.""" 682 683 name = 'find-provisioning-profile' 684 help = 'find provisioning profile for use by Xcode project generator' 685 686 @staticmethod 687 def _Register(parser): 688 parser.add_argument('--bundle-id', 689 '-b', 690 required=True, 691 help='bundle identifier') 692 parser.add_argument( 693 '--mobileprovision', 694 '-m', 695 action='append', 696 default=[], 697 dest='mobileprovision_files', 698 help='set of mobileprovision files to use. If empty, uses the files ' + 699 'in $HOME/Library/MobileDevice/Provisioning Profiles') 700 701 @staticmethod 702 def _Execute(args): 703 provisioning_profile_info = {} 704 provisioning_profile = FindProvisioningProfile(args.mobileprovision_files, 705 args.bundle_id, False) 706 for key in ('team_identifier', 'name', 'path'): 707 if provisioning_profile: 708 provisioning_profile_info[key] = getattr(provisioning_profile, key) 709 else: 710 provisioning_profile_info[key] = '' 711 print(json.dumps(provisioning_profile_info)) 712 713 714 def Main(): 715 # Cache this codec so that plistlib can find it. See 716 # https://crbug.com/999461#c12 for more details. 717 codecs.lookup('utf-8') 718 719 parser = argparse.ArgumentParser('codesign iOS bundles') 720 subparsers = parser.add_subparsers() 721 722 actions = [ 723 CodeSignBundleAction, 724 CodeSignFileAction, 725 GenerateEntitlementsAction, 726 FindProvisioningProfileAction, 727 ] 728 729 for action in actions: 730 action.Register(subparsers) 731 732 args = parser.parse_args() 733 args.func(args) 734 735 736 if __name__ == '__main__': 737 sys.exit(Main())