blob: 5e4814d9b767dff6ef73b835acf343f981bb9f2d [file] [log] [blame]
#!/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())