| #!/usr/bin/python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2022 The Tulsi Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Script responsible for resigning test artifacts so they can run on device.""" |
| |
| import json |
| import os |
| import subprocess |
| import sys |
| |
| # List of frameworks that Xcode injects into test host targets that should be |
| # re-signed when running the tests on devices. |
| XCODE_INJECTED_FRAMEWORKS = [ |
| 'libXCTestBundleInject.dylib', |
| 'libXCTestSwiftSupport.dylib', |
| 'IDEBundleInjection.framework', |
| 'XCTAutomationSupport.framework', |
| 'XCTest.framework', |
| 'XCTestCore.framework', |
| 'XCTestSupport.framework', |
| 'XCUnit.framework', |
| 'XCUIAutomation.framework', |
| ] |
| |
| |
| def _PrintUnbuffered(msg): |
| sys.stdout.write('%s\n' % msg) |
| sys.stdout.flush() |
| |
| |
| def _PrintXcodeWarning(msg): |
| sys.stdout.write(':: warning: %s\n' % msg) |
| sys.stdout.flush() |
| |
| |
| def _PrintXcodeError(msg): |
| sys.stderr.write(':: error: %s\n' % msg) |
| sys.stderr.flush() |
| |
| |
| def _RunSubprocess(cmd): |
| """Runs the given command as a subprocess, returning (exit_code, output).""" |
| _PrintUnbuffered('Running %r' % cmd) |
| process = subprocess.Popen( |
| cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| output, _ = process.communicate() |
| return (process.returncode, output) |
| |
| |
| def _ResignBundle(bundle_path, signing_identity, entitlements=None): |
| """Re-signs the bundle with the given signing identity and entitlements.""" |
| |
| command = [ |
| 'xcrun', |
| 'codesign', |
| '-f', |
| '--timestamp=none', |
| '-s', |
| signing_identity, |
| ] |
| |
| if entitlements: |
| command.extend(['--entitlements', entitlements]) |
| else: |
| command.append('--preserve-metadata=entitlements') |
| |
| command.append(bundle_path) |
| |
| returncode, output = _RunSubprocess(command) |
| if returncode: |
| _PrintXcodeError('Re-sign command %r failed. %s' % (command, output)) |
| return returncode |
| return 0 |
| |
| |
| def _ResignXcodeTestFrameworks(bundle, signing_identity): |
| """Re-signs the support frameworks injected by Xcode in the given bundle.""" |
| for framework in XCODE_INJECTED_FRAMEWORKS: |
| framework_path = os.path.join(bundle, 'Frameworks', framework) |
| if os.path.isdir(framework_path) or os.path.isfile(framework_path): |
| exit_code = _ResignBundle(framework_path, signing_identity) |
| if exit_code != 0: |
| return exit_code |
| return 0 |
| |
| |
| class FrameworksResigningOperation(object): |
| """Represents a resigning operation for the test frameworks of a bundle.""" |
| |
| def __init__(self, bundle_path, signing_identity): |
| """All arguments are required and non-None.""" |
| self.bundle_path = bundle_path |
| self.signing_identity = signing_identity |
| |
| def __str__(self): |
| return 'FrameworksResigningOperation: sign %s using identity %s' % ( |
| self.bundle_path, self.signing_identity) |
| |
| def Perform(self): |
| return _ResignXcodeTestFrameworks(self.bundle_path, self.signing_identity) |
| |
| |
| class BundleResigningOperation(object): |
| """Represents a resigning operation for a bundle.""" |
| |
| def __init__(self, bundle_path, signing_identity, entitlements=None): |
| """If the entitlements arg is not given, entitlements are preserved.""" |
| self.bundle_path = bundle_path |
| self.signing_identity = signing_identity |
| self.entitlements = entitlements |
| |
| def __str__(self): |
| return ('BundleResigningOperation: sign %s using identity %s and ' |
| 'entitlements %s') % ( |
| self.bundle_path, self.signing_identity, self.entitlements) |
| |
| def Perform(self): |
| return _ResignBundle(self.bundle_path, self.signing_identity, |
| self.entitlements) |
| |
| |
| class OperationsSerialization(object): |
| """Handles serialization of resigning operations.""" |
| |
| BUNDLE_OPERATION = 'Bundle' |
| FRAMEWORKS_OPERATION = 'Frameworks' |
| |
| @staticmethod |
| def OperationsToJson(operations): |
| """Convert the list of operations to a JSON list.""" |
| if not isinstance(operations, list): |
| raise TypeError('Operations is not a list: %s' % str(operations)) |
| return [OperationsSerialization.OperationToJson(op) for op in operations] |
| |
| @staticmethod |
| def OperationToJson(operation): |
| """Convert the operation to a JSON dictionary.""" |
| if isinstance(operation, FrameworksResigningOperation): |
| return { |
| 'type': OperationsSerialization.FRAMEWORKS_OPERATION, |
| 'bundle_path': operation.bundle_path, |
| 'signing_identity': operation.signing_identity |
| } |
| if isinstance(operation, BundleResigningOperation): |
| return { |
| 'type': OperationsSerialization.BUNDLE_OPERATION, |
| 'bundle_path': operation.bundle_path, |
| 'signing_identity': operation.signing_identity, |
| 'entitlements': operation.entitlements |
| } |
| raise TypeError('Unknown resign operation: %s' % str(operation)) |
| |
| @staticmethod |
| def JsonToOperations(json_obj): |
| """Convert the JSON list into a list of operations.""" |
| if not isinstance(json_obj, list): |
| raise TypeError('Json operations object is not a list: %s' % |
| str(json_obj)) |
| return [OperationsSerialization.JsonToOperation(obj) for obj in json_obj] |
| |
| @staticmethod |
| def JsonToOperation(json_obj): |
| """Convert the JSON dictionary to an operation.""" |
| if not isinstance(json_obj, dict): |
| raise TypeError('Json operation object is not a dict: %s' % str(json_obj)) |
| operation_type = json_obj.get('type', None) |
| if operation_type == OperationsSerialization.FRAMEWORKS_OPERATION: |
| return FrameworksResigningOperation(json_obj['bundle_path'], |
| json_obj['signing_identity']) |
| elif operation_type == OperationsSerialization.BUNDLE_OPERATION: |
| return BundleResigningOperation(json_obj['bundle_path'], |
| json_obj['signing_identity'], |
| json_obj['entitlements']) |
| else: |
| raise TypeError('Invalid operation type: %s' % operation_type) |
| |
| |
| def PerformOperations(operations): |
| """Perform the given resigning operations.""" |
| for operation in operations: |
| returncode = operation.Perform() |
| if returncode: |
| _PrintXcodeError('Resign operation failed with %d: %s' % |
| (returncode, operation)) |
| return returncode |
| return 0 |
| |
| |
| def main(): |
| # Only need to resign artifacts for device. |
| platform_name = os.environ['PLATFORM_NAME'] |
| if platform_name.endswith('simulator'): |
| return 0 |
| |
| resign_manifest_path = os.environ['TULSI_RESIGN_MANIFEST'] |
| with open(resign_manifest_path) as manifest_file: |
| resign_manifest = json.load(manifest_file) |
| operations = OperationsSerialization.JsonToOperations(resign_manifest) |
| return PerformOperations(operations) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |