diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /third_party/libwebrtc/build/config/ios/codesign.py | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/build/config/ios/codesign.py')
-rw-r--r-- | third_party/libwebrtc/build/config/ios/codesign.py | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/config/ios/codesign.py b/third_party/libwebrtc/build/config/ios/codesign.py new file mode 100644 index 0000000000..15d25a78df --- /dev/null +++ b/third_party/libwebrtc/build/config/ios/codesign.py @@ -0,0 +1,691 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +from __future__ import print_function + +import argparse +import codecs +import datetime +import fnmatch +import glob +import json +import os +import plistlib +import shutil +import subprocess +import sys +import tempfile + +if sys.version_info.major < 3: + basestring_compat = basestring +else: + basestring_compat = str + + +def GetProvisioningProfilesDir(): + """Returns the location of the installed mobile provisioning profiles. + + Returns: + The path to the directory containing the installed mobile provisioning + profiles as a string. + """ + return os.path.join( + os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') + + +def ReadPlistFromString(plist_bytes): + """Parse property list from given |plist_bytes|. + + Args: + plist_bytes: contents of property list to load. Must be bytes in python 3. + + Returns: + The contents of property list as a python object. + """ + if sys.version_info.major == 2: + return plistlib.readPlistFromString(plist_bytes) + else: + return plistlib.loads(plist_bytes) + + +def LoadPlistFile(plist_path): + """Loads property list file at |plist_path|. + + Args: + plist_path: path to the property list file to load. + + Returns: + The content of the property list file as a python object. + """ + if sys.version_info.major == 2: + return plistlib.readPlistFromString( + subprocess.check_output( + ['xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path])) + else: + with open(plist_path, 'rb') as fp: + return plistlib.load(fp) + + +def CreateSymlink(value, location): + """Creates symlink with value at location if the target exists.""" + target = os.path.join(os.path.dirname(location), value) + if os.path.exists(location): + os.unlink(location) + os.symlink(value, location) + + +class Bundle(object): + """Wraps a bundle.""" + + def __init__(self, bundle_path, platform): + """Initializes the Bundle object with data from bundle Info.plist file.""" + self._path = bundle_path + self._kind = Bundle.Kind(platform, os.path.splitext(bundle_path)[-1]) + self._data = None + + def Load(self): + self._data = LoadPlistFile(self.info_plist_path) + + @staticmethod + def Kind(platform, extension): + if platform == 'iphonesimulator' or platform == 'iphoneos': + return 'ios' + if platform == 'macosx': + if extension == '.framework': + return 'mac_framework' + return 'mac' + raise ValueError('unknown bundle type %s for %s' % (extension, platform)) + + @property + def kind(self): + return self._kind + + @property + def path(self): + return self._path + + @property + def contents_dir(self): + if self._kind == 'mac': + return os.path.join(self.path, 'Contents') + if self._kind == 'mac_framework': + return os.path.join(self.path, 'Versions/A') + return self.path + + @property + def executable_dir(self): + if self._kind == 'mac': + return os.path.join(self.contents_dir, 'MacOS') + return self.contents_dir + + @property + def resources_dir(self): + if self._kind == 'mac' or self._kind == 'mac_framework': + return os.path.join(self.contents_dir, 'Resources') + return self.path + + @property + def info_plist_path(self): + if self._kind == 'mac_framework': + return os.path.join(self.resources_dir, 'Info.plist') + return os.path.join(self.contents_dir, 'Info.plist') + + @property + def signature_dir(self): + return os.path.join(self.contents_dir, '_CodeSignature') + + @property + def identifier(self): + return self._data['CFBundleIdentifier'] + + @property + def binary_name(self): + return self._data['CFBundleExecutable'] + + @property + def binary_path(self): + return os.path.join(self.executable_dir, self.binary_name) + + def Validate(self, expected_mappings): + """Checks that keys in the bundle have the expected value. + + Args: + expected_mappings: a dictionary of string to object, each mapping will + be looked up in the bundle data to check it has the same value (missing + values will be ignored) + + Returns: + A dictionary of the key with a different value between expected_mappings + and the content of the bundle (i.e. errors) so that caller can format the + error message. The dictionary will be empty if there are no errors. + """ + errors = {} + for key, expected_value in expected_mappings.items(): + if key in self._data: + value = self._data[key] + if value != expected_value: + errors[key] = (value, expected_value) + return errors + + +class ProvisioningProfile(object): + """Wraps a mobile provisioning profile file.""" + + def __init__(self, provisioning_profile_path): + """Initializes the ProvisioningProfile with data from profile file.""" + self._path = provisioning_profile_path + self._data = ReadPlistFromString( + subprocess.check_output([ + 'xcrun', 'security', 'cms', '-D', '-u', 'certUsageAnyCA', '-i', + provisioning_profile_path + ])) + + @property + def path(self): + return self._path + + @property + def team_identifier(self): + return self._data.get('TeamIdentifier', [''])[0] + + @property + def name(self): + return self._data.get('Name', '') + + @property + def application_identifier_pattern(self): + return self._data.get('Entitlements', {}).get('application-identifier', '') + + @property + def application_identifier_prefix(self): + return self._data.get('ApplicationIdentifierPrefix', [''])[0] + + @property + def entitlements(self): + return self._data.get('Entitlements', {}) + + @property + def expiration_date(self): + return self._data.get('ExpirationDate', datetime.datetime.now()) + + def ValidToSignBundle(self, bundle_identifier): + """Checks whether the provisioning profile can sign bundle_identifier. + + Args: + bundle_identifier: the identifier of the bundle that needs to be signed. + + Returns: + True if the mobile provisioning profile can be used to sign a bundle + with the corresponding bundle_identifier, False otherwise. + """ + return fnmatch.fnmatch( + '%s.%s' % (self.application_identifier_prefix, bundle_identifier), + self.application_identifier_pattern) + + def Install(self, installation_path): + """Copies mobile provisioning profile info to |installation_path|.""" + shutil.copy2(self.path, installation_path) + + +class Entitlements(object): + """Wraps an Entitlement plist file.""" + + def __init__(self, entitlements_path): + """Initializes Entitlements object from entitlement file.""" + self._path = entitlements_path + self._data = LoadPlistFile(self._path) + + @property + def path(self): + return self._path + + def ExpandVariables(self, substitutions): + self._data = self._ExpandVariables(self._data, substitutions) + + def _ExpandVariables(self, data, substitutions): + if isinstance(data, basestring_compat): + for key, substitution in substitutions.items(): + data = data.replace('$(%s)' % (key,), substitution) + return data + + if isinstance(data, dict): + for key, value in data.items(): + data[key] = self._ExpandVariables(value, substitutions) + return data + + if isinstance(data, list): + for i, value in enumerate(data): + data[i] = self._ExpandVariables(value, substitutions) + + return data + + def LoadDefaults(self, defaults): + for key, value in defaults.items(): + if key not in self._data: + self._data[key] = value + + def WriteTo(self, target_path): + with open(target_path, 'wb') as fp: + if sys.version_info.major == 2: + plistlib.writePlist(self._data, fp) + else: + plistlib.dump(self._data, fp) + + +def FindProvisioningProfile(bundle_identifier, required): + """Finds mobile provisioning profile to use to sign bundle. + + Args: + bundle_identifier: the identifier of the bundle to sign. + + Returns: + The ProvisioningProfile object that can be used to sign the Bundle + object or None if no matching provisioning profile was found. + """ + provisioning_profile_paths = glob.glob( + os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision')) + + # Iterate over all installed mobile provisioning profiles and filter those + # that can be used to sign the bundle, ignoring expired ones. + now = datetime.datetime.now() + valid_provisioning_profiles = [] + one_hour = datetime.timedelta(0, 3600) + for provisioning_profile_path in provisioning_profile_paths: + provisioning_profile = ProvisioningProfile(provisioning_profile_path) + if provisioning_profile.expiration_date - now < one_hour: + sys.stderr.write( + 'Warning: ignoring expired provisioning profile: %s.\n' % + provisioning_profile_path) + continue + if provisioning_profile.ValidToSignBundle(bundle_identifier): + valid_provisioning_profiles.append(provisioning_profile) + + if not valid_provisioning_profiles: + if required: + sys.stderr.write( + 'Error: no mobile provisioning profile found for "%s".\n' % + bundle_identifier) + sys.exit(1) + return None + + # Select the most specific mobile provisioning profile, i.e. the one with + # the longest application identifier pattern (prefer the one with the latest + # expiration date as a secondary criteria). + selected_provisioning_profile = max( + valid_provisioning_profiles, + key=lambda p: (len(p.application_identifier_pattern), p.expiration_date)) + + one_week = datetime.timedelta(7) + if selected_provisioning_profile.expiration_date - now < 2 * one_week: + sys.stderr.write( + 'Warning: selected provisioning profile will expire soon: %s' % + selected_provisioning_profile.path) + return selected_provisioning_profile + + +def CodeSignBundle(bundle_path, identity, extra_args): + process = subprocess.Popen( + ['xcrun', 'codesign', '--force', '--sign', identity, '--timestamp=none'] + + list(extra_args) + [bundle_path], + stderr=subprocess.PIPE, + universal_newlines=True) + _, stderr = process.communicate() + if process.returncode: + sys.stderr.write(stderr) + sys.exit(process.returncode) + for line in stderr.splitlines(): + if line.endswith(': replacing existing signature'): + # Ignore warning about replacing existing signature as this should only + # happen when re-signing system frameworks (and then it is expected). + continue + sys.stderr.write(line) + sys.stderr.write('\n') + + +def InstallSystemFramework(framework_path, bundle_path, args): + """Install framework from |framework_path| to |bundle| and code-re-sign it.""" + installed_framework_path = os.path.join( + bundle_path, 'Frameworks', os.path.basename(framework_path)) + + if os.path.isfile(framework_path): + shutil.copy(framework_path, installed_framework_path) + elif os.path.isdir(framework_path): + if os.path.exists(installed_framework_path): + shutil.rmtree(installed_framework_path) + shutil.copytree(framework_path, installed_framework_path) + + CodeSignBundle(installed_framework_path, args.identity, + ['--deep', '--preserve-metadata=identifier,entitlements,flags']) + + +def GenerateEntitlements(path, provisioning_profile, bundle_identifier): + """Generates an entitlements file. + + Args: + path: path to the entitlements template file + provisioning_profile: ProvisioningProfile object to use, may be None + bundle_identifier: identifier of the bundle to sign. + """ + entitlements = Entitlements(path) + if provisioning_profile: + entitlements.LoadDefaults(provisioning_profile.entitlements) + app_identifier_prefix = \ + provisioning_profile.application_identifier_prefix + '.' + else: + app_identifier_prefix = '*.' + entitlements.ExpandVariables({ + 'CFBundleIdentifier': bundle_identifier, + 'AppIdentifierPrefix': app_identifier_prefix, + }) + return entitlements + + +def GenerateBundleInfoPlist(bundle, plist_compiler, partial_plist): + """Generates the bundle Info.plist for a list of partial .plist files. + + Args: + bundle: a Bundle instance + plist_compiler: string, path to the Info.plist compiler + partial_plist: list of path to partial .plist files to merge + """ + + # Filter empty partial .plist files (this happens if an application + # does not compile any asset catalog, in which case the partial .plist + # file from the asset catalog compilation step is just a stamp file). + filtered_partial_plist = [] + for plist in partial_plist: + plist_size = os.stat(plist).st_size + if plist_size: + filtered_partial_plist.append(plist) + + # Invoke the plist_compiler script. It needs to be a python script. + subprocess.check_call([ + 'python', + plist_compiler, + 'merge', + '-f', + 'binary1', + '-o', + bundle.info_plist_path, + ] + filtered_partial_plist) + + +class Action(object): + """Class implementing one action supported by the script.""" + + @classmethod + def Register(cls, subparsers): + parser = subparsers.add_parser(cls.name, help=cls.help) + parser.set_defaults(func=cls._Execute) + cls._Register(parser) + + +class CodeSignBundleAction(Action): + """Class implementing the code-sign-bundle action.""" + + name = 'code-sign-bundle' + help = 'perform code signature for a bundle' + + @staticmethod + def _Register(parser): + parser.add_argument( + '--entitlements', '-e', dest='entitlements_path', + help='path to the entitlements file to use') + parser.add_argument( + 'path', help='path to the iOS bundle to codesign') + parser.add_argument( + '--identity', '-i', required=True, + help='identity to use to codesign') + parser.add_argument( + '--binary', '-b', required=True, + help='path to the iOS bundle binary') + parser.add_argument( + '--framework', '-F', action='append', default=[], dest='frameworks', + help='install and resign system framework') + parser.add_argument( + '--disable-code-signature', action='store_true', dest='no_signature', + help='disable code signature') + parser.add_argument( + '--disable-embedded-mobileprovision', action='store_false', + default=True, dest='embedded_mobileprovision', + help='disable finding and embedding mobileprovision') + parser.add_argument( + '--platform', '-t', required=True, + help='platform the signed bundle is targeting') + parser.add_argument( + '--partial-info-plist', '-p', action='append', default=[], + help='path to partial Info.plist to merge to create bundle Info.plist') + parser.add_argument( + '--plist-compiler-path', '-P', action='store', + help='path to the plist compiler script (for --partial-info-plist)') + parser.set_defaults(no_signature=False) + + @staticmethod + def _Execute(args): + if not args.identity: + args.identity = '-' + + bundle = Bundle(args.path, args.platform) + + if args.partial_info_plist: + GenerateBundleInfoPlist(bundle, args.plist_compiler_path, + args.partial_info_plist) + + # The bundle Info.plist may have been updated by GenerateBundleInfoPlist() + # above. Load the bundle information from Info.plist after the modification + # have been written to disk. + bundle.Load() + + # According to Apple documentation, the application binary must be the same + # as the bundle name without the .app suffix. See crbug.com/740476 for more + # information on what problem this can cause. + # + # To prevent this class of error, fail with an error if the binary name is + # incorrect in the Info.plist as it is not possible to update the value in + # Info.plist at this point (the file has been copied by a different target + # and ninja would consider the build dirty if it was updated). + # + # Also checks that the name of the bundle is correct too (does not cause the + # build to be considered dirty, but still terminate the script in case of an + # incorrect bundle name). + # + # Apple documentation is available at: + # https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html + bundle_name = os.path.splitext(os.path.basename(bundle.path))[0] + errors = bundle.Validate({ + 'CFBundleName': bundle_name, + 'CFBundleExecutable': bundle_name, + }) + if errors: + for key in sorted(errors): + value, expected_value = errors[key] + sys.stderr.write('%s: error: %s value incorrect: %s != %s\n' % ( + bundle.path, key, value, expected_value)) + sys.stderr.flush() + sys.exit(1) + + # Delete existing embedded mobile provisioning. + embedded_provisioning_profile = os.path.join( + bundle.path, 'embedded.mobileprovision') + if os.path.isfile(embedded_provisioning_profile): + os.unlink(embedded_provisioning_profile) + + # Delete existing code signature. + if os.path.exists(bundle.signature_dir): + shutil.rmtree(bundle.signature_dir) + + # Install system frameworks if requested. + for framework_path in args.frameworks: + InstallSystemFramework(framework_path, args.path, args) + + # Copy main binary into bundle. + if not os.path.isdir(bundle.executable_dir): + os.makedirs(bundle.executable_dir) + shutil.copy(args.binary, bundle.binary_path) + + if bundle.kind == 'mac_framework': + # Create Versions/Current -> Versions/A symlink + CreateSymlink('A', os.path.join(bundle.path, 'Versions/Current')) + + # Create $binary_name -> Versions/Current/$binary_name symlink + CreateSymlink(os.path.join('Versions/Current', bundle.binary_name), + os.path.join(bundle.path, bundle.binary_name)) + + # Create optional symlinks. + for name in ('Headers', 'Resources', 'Modules'): + target = os.path.join(bundle.path, 'Versions/A', name) + if os.path.exists(target): + CreateSymlink(os.path.join('Versions/Current', name), + os.path.join(bundle.path, name)) + else: + obsolete_path = os.path.join(bundle.path, name) + if os.path.exists(obsolete_path): + os.unlink(obsolete_path) + + if args.no_signature: + return + + codesign_extra_args = [] + + if args.embedded_mobileprovision: + # Find mobile provisioning profile and embeds it into the bundle (if a + # code signing identify has been provided, fails if no valid mobile + # provisioning is found). + provisioning_profile_required = args.identity != '-' + provisioning_profile = FindProvisioningProfile( + bundle.identifier, provisioning_profile_required) + if provisioning_profile and args.platform != 'iphonesimulator': + provisioning_profile.Install(embedded_provisioning_profile) + + if args.entitlements_path is not None: + temporary_entitlements_file = \ + tempfile.NamedTemporaryFile(suffix='.xcent') + codesign_extra_args.extend( + ['--entitlements', temporary_entitlements_file.name]) + + entitlements = GenerateEntitlements( + args.entitlements_path, provisioning_profile, bundle.identifier) + entitlements.WriteTo(temporary_entitlements_file.name) + + CodeSignBundle(bundle.path, args.identity, codesign_extra_args) + + +class CodeSignFileAction(Action): + """Class implementing code signature for a single file.""" + + name = 'code-sign-file' + help = 'code-sign a single file' + + @staticmethod + def _Register(parser): + parser.add_argument( + 'path', help='path to the file to codesign') + parser.add_argument( + '--identity', '-i', required=True, + help='identity to use to codesign') + parser.add_argument( + '--output', '-o', + help='if specified copy the file to that location before signing it') + parser.set_defaults(sign=True) + + @staticmethod + def _Execute(args): + if not args.identity: + args.identity = '-' + + install_path = args.path + if args.output: + + if os.path.isfile(args.output): + os.unlink(args.output) + elif os.path.isdir(args.output): + shutil.rmtree(args.output) + + if os.path.isfile(args.path): + shutil.copy(args.path, args.output) + elif os.path.isdir(args.path): + shutil.copytree(args.path, args.output) + + install_path = args.output + + CodeSignBundle(install_path, args.identity, + ['--deep', '--preserve-metadata=identifier,entitlements']) + + +class GenerateEntitlementsAction(Action): + """Class implementing the generate-entitlements action.""" + + name = 'generate-entitlements' + help = 'generate entitlements file' + + @staticmethod + def _Register(parser): + parser.add_argument( + '--entitlements', '-e', dest='entitlements_path', + help='path to the entitlements file to use') + parser.add_argument( + 'path', help='path to the entitlements file to generate') + parser.add_argument( + '--info-plist', '-p', required=True, + help='path to the bundle Info.plist') + + @staticmethod + def _Execute(args): + info_plist = LoadPlistFile(args.info_plist) + bundle_identifier = info_plist['CFBundleIdentifier'] + provisioning_profile = FindProvisioningProfile(bundle_identifier, False) + entitlements = GenerateEntitlements( + args.entitlements_path, provisioning_profile, bundle_identifier) + entitlements.WriteTo(args.path) + + +class FindProvisioningProfileAction(Action): + """Class implementing the find-codesign-identity action.""" + + name = 'find-provisioning-profile' + help = 'find provisioning profile for use by Xcode project generator' + + @staticmethod + def _Register(parser): + parser.add_argument('--bundle-id', + '-b', + required=True, + help='bundle identifier') + + @staticmethod + def _Execute(args): + provisioning_profile_info = {} + provisioning_profile = FindProvisioningProfile(args.bundle_id, False) + for key in ('team_identifier', 'name'): + if provisioning_profile: + provisioning_profile_info[key] = getattr(provisioning_profile, key) + else: + provisioning_profile_info[key] = '' + print(json.dumps(provisioning_profile_info)) + + +def Main(): + # Cache this codec so that plistlib can find it. See + # https://crbug.com/999461#c12 for more details. + codecs.lookup('utf-8') + + parser = argparse.ArgumentParser('codesign iOS bundles') + subparsers = parser.add_subparsers() + + actions = [ + CodeSignBundleAction, + CodeSignFileAction, + GenerateEntitlementsAction, + FindProvisioningProfileAction, + ] + + for action in actions: + action.Register(subparsers) + + args = parser.parse_args() + args.func(args) + + +if __name__ == '__main__': + sys.exit(Main()) |