|  | #!/usr/bin/env python | 
|  |  | 
|  | # 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 plistlib | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  | import shlex | 
|  |  | 
|  |  | 
|  | # 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.iteritems()} | 
|  | if isinstance(value, list): | 
|  | return [Interpolate(v, substitutions) for v in value] | 
|  | if isinstance(value, str): | 
|  | return InterpolateString(value, substitutions) | 
|  | return value | 
|  |  | 
|  |  | 
|  | def LoadPList(path): | 
|  | """Loads Plist at |path| and returns it as a dictionary.""" | 
|  | fd, name = tempfile.mkstemp() | 
|  | try: | 
|  | subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) | 
|  | with os.fdopen(fd, 'r') as f: | 
|  | return plistlib.readPlist(f) | 
|  | finally: | 
|  | os.unlink(name) | 
|  |  | 
|  |  | 
|  | def SavePList(path, format, data): | 
|  | """Saves |data| as a Plist to |path| in the specified |format|.""" | 
|  | fd, name = tempfile.mkstemp() | 
|  | try: | 
|  | # "plutil" 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) | 
|  | with os.fdopen(fd, 'w') as f: | 
|  | plistlib.writePlist(data, f) | 
|  | subprocess.check_call(['plutil', '-convert', format, '-o', path, name]) | 
|  | finally: | 
|  | os.unlink(name) | 
|  |  | 
|  |  | 
|  | 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.iteritems(): | 
|  | 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', 'json'), | 
|  | help='format of the plist file to generate') | 
|  | 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', 'json'), | 
|  | help='format of the plist file to generate') | 
|  |  | 
|  | @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(): | 
|  | 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__': | 
|  | sys.exit(Main()) |