diff options
Diffstat (limited to 'third_party/libwebrtc/build/apple/plist_util.py')
-rw-r--r-- | third_party/libwebrtc/build/apple/plist_util.py | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/apple/plist_util.py b/third_party/libwebrtc/build/apple/plist_util.py new file mode 100644 index 0000000000..54cf46176b --- /dev/null +++ b/third_party/libwebrtc/build/apple/plist_util.py @@ -0,0 +1,265 @@ +# 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. + +import argparse +import codecs +import plistlib +import os +import re +import subprocess +import sys +import tempfile +import shlex + +if sys.version_info.major < 3: + basestring_compat = basestring +else: + basestring_compat = str + +# Xcode substitutes variables like ${PRODUCT_NAME} or $(PRODUCT_NAME) when +# compiling Info.plist. It also supports supports modifiers like :identifier +# or :rfc1034identifier. SUBSTITUTION_REGEXP_LIST is a list of regular +# expressions matching a variable substitution pattern with an optional +# modifier, while INVALID_CHARACTER_REGEXP matches all characters that are +# not valid in an "identifier" value (used when applying the modifier). +INVALID_CHARACTER_REGEXP = re.compile(r'[_/\s]') +SUBSTITUTION_REGEXP_LIST = ( + re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}'), + re.compile(r'\$\((?P<id>[^}]*?)(?P<modifier>:[^}]*)?\)'), +) + + +class SubstitutionError(Exception): + def __init__(self, key): + super(SubstitutionError, self).__init__() + self.key = key + + def __str__(self): + return "SubstitutionError: {}".format(self.key) + + +def InterpolateString(value, substitutions): + """Interpolates variable references into |value| using |substitutions|. + + Inputs: + value: a string + substitutions: a mapping of variable names to values + + Returns: + A new string with all variables references ${VARIABLES} replaced by their + value in |substitutions|. Raises SubstitutionError if a variable has no + substitution. + """ + + def repl(match): + variable = match.group('id') + if variable not in substitutions: + raise SubstitutionError(variable) + # Some values need to be identifier and thus the variables references may + # contains :modifier attributes to indicate how they should be converted + # to identifiers ("identifier" replaces all invalid characters by '_' and + # "rfc1034identifier" replaces them by "-" to make valid URI too). + modifier = match.group('modifier') + if modifier == ':identifier': + return INVALID_CHARACTER_REGEXP.sub('_', substitutions[variable]) + elif modifier == ':rfc1034identifier': + return INVALID_CHARACTER_REGEXP.sub('-', substitutions[variable]) + else: + return substitutions[variable] + + for substitution_regexp in SUBSTITUTION_REGEXP_LIST: + value = substitution_regexp.sub(repl, value) + return value + + +def Interpolate(value, substitutions): + """Interpolates variable references into |value| using |substitutions|. + + Inputs: + value: a value, can be a dictionary, list, string or other + substitutions: a mapping of variable names to values + + Returns: + A new value with all variables references ${VARIABLES} replaced by their + value in |substitutions|. Raises SubstitutionError if a variable has no + substitution. + """ + if isinstance(value, dict): + return {k: Interpolate(v, substitutions) for k, v in value.items()} + if isinstance(value, list): + return [Interpolate(v, substitutions) for v in value] + if isinstance(value, basestring_compat): + return InterpolateString(value, substitutions) + return value + + +def LoadPList(path): + """Loads Plist at |path| and returns it as a dictionary.""" + if sys.version_info.major == 2: + fd, name = tempfile.mkstemp() + try: + subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) + with os.fdopen(fd, 'rb') as f: + return plistlib.readPlist(f) + finally: + os.unlink(name) + else: + with open(path, 'rb') as f: + return plistlib.load(f) + + +def SavePList(path, format, data): + """Saves |data| as a Plist to |path| in the specified |format|.""" + # The below does not replace the destination file but update it in place, + # so if more than one hardlink points to destination all of them will be + # modified. This is not what is expected, so delete destination file if + # it does exist. + if os.path.exists(path): + os.unlink(path) + if sys.version_info.major == 2: + fd, name = tempfile.mkstemp() + try: + with os.fdopen(fd, 'wb') as f: + plistlib.writePlist(data, f) + subprocess.check_call(['plutil', '-convert', format, '-o', path, name]) + finally: + os.unlink(name) + else: + with open(path, 'wb') as f: + plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML} + plistlib.dump(data, f, fmt=plist_format[format]) + + +def MergePList(plist1, plist2): + """Merges |plist1| with |plist2| recursively. + + Creates a new dictionary representing a Property List (.plist) files by + merging the two dictionary |plist1| and |plist2| recursively (only for + dictionary values). List value will be concatenated. + + Args: + plist1: a dictionary representing a Property List (.plist) file + plist2: a dictionary representing a Property List (.plist) file + + Returns: + A new dictionary representing a Property List (.plist) file by merging + |plist1| with |plist2|. If any value is a dictionary, they are merged + recursively, otherwise |plist2| value is used. If values are list, they + are concatenated. + """ + result = plist1.copy() + for key, value in plist2.items(): + if isinstance(value, dict): + old_value = result.get(key) + if isinstance(old_value, dict): + value = MergePList(old_value, value) + if isinstance(value, list): + value = plist1.get(key, []) + plist2.get(key, []) + result[key] = value + return result + + +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 MergeAction(Action): + """Class to merge multiple plist files.""" + + name = 'merge' + help = 'merge multiple plist files' + + @staticmethod + def _Register(parser): + parser.add_argument('-o', + '--output', + required=True, + help='path to the output plist file') + parser.add_argument('-f', + '--format', + required=True, + choices=('xml1', 'binary1'), + help='format of the plist file to generate') + parser.add_argument( + '-x', + '--xcode-version', + help='version of Xcode, ignored (can be used to force rebuild)') + parser.add_argument('path', nargs="+", help='path to plist files to merge') + + @staticmethod + def _Execute(args): + data = {} + for filename in args.path: + data = MergePList(data, LoadPList(filename)) + SavePList(args.output, args.format, data) + + +class SubstituteAction(Action): + """Class implementing the variable substitution in a plist file.""" + + name = 'substitute' + help = 'perform pattern substitution in a plist file' + + @staticmethod + def _Register(parser): + parser.add_argument('-o', + '--output', + required=True, + help='path to the output plist file') + parser.add_argument('-t', + '--template', + required=True, + help='path to the template file') + parser.add_argument('-s', + '--substitution', + action='append', + default=[], + help='substitution rule in the format key=value') + parser.add_argument('-f', + '--format', + required=True, + choices=('xml1', 'binary1'), + help='format of the plist file to generate') + parser.add_argument( + '-x', + '--xcode-version', + help='version of Xcode, ignored (can be used to force rebuild)') + + @staticmethod + def _Execute(args): + substitutions = {} + for substitution in args.substitution: + key, value = substitution.split('=', 1) + substitutions[key] = value + data = Interpolate(LoadPList(args.template), substitutions) + SavePList(args.output, args.format, data) + + +def Main(): + # Cache this codec so that plistlib can find it. See + # https://crbug.com/1005190#c2 for more details. + codecs.lookup('utf-8') + + parser = argparse.ArgumentParser(description='manipulate plist files') + subparsers = parser.add_subparsers() + + for action in [MergeAction, SubstituteAction]: + action.Register(subparsers) + + args = parser.parse_args() + args.func(args) + + +if __name__ == '__main__': + # TODO(https://crbug.com/941669): Temporary workaround until all scripts use + # python3 by default. + if sys.version_info[0] < 3: + os.execvp('python3', ['python3'] + sys.argv) + sys.exit(Main()) |