| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import argparse |
| 6 import plistlib |
| 7 import os |
| 8 import re |
| 9 import subprocess |
| 10 import sys |
| 11 import tempfile |
| 12 import shlex |
| 13 |
| 14 |
| 15 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. |
| 16 # It also supports supports modifiers like :identifier or :rfc1034identifier. |
| 17 # SUBST_RE matches a variable substitution pattern with an optional modifier, |
| 18 # while IDENT_RE matches all characters that are not valid in an "identifier" |
| 19 # value (used when applying the modifier). |
| 20 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') |
| 21 IDENT_RE = re.compile(r'[/\s]') |
| 22 |
| 23 |
| 24 class ArgumentParser(argparse.ArgumentParser): |
| 25 """Subclass of argparse.ArgumentParser to work with GN response files. |
| 26 |
| 27 GN response file writes all the arguments on a single line and assumes |
| 28 that the python script uses shlext.split() to extract them. Since the |
| 29 default ArgumentParser expects a single argument per line, we need to |
| 30 provide a subclass to have the correct support for @{{response_file_name}}. |
| 31 """ |
| 32 |
| 33 def convert_arg_line_to_args(self, arg_line): |
| 34 return shlex.split(arg_line) |
| 35 |
| 36 |
| 37 def Merge(plist1, plist2): |
| 38 """Recursively merges |plist2| into |plist1|.""" |
| 39 if not plist1: |
| 40 return plist2 |
| 41 for key in plist2: |
| 42 value = plist2[key] |
| 43 if isinstance(value, dict): |
| 44 plist1[key] = Merge(plist1.get(key, {}), value) |
| 45 else: |
| 46 plist1[key] = value |
| 47 return plist1 |
| 48 |
| 49 |
| 50 def Interpolate(plist, substitutions): |
| 51 """Interpolates ${variable} into |plist| using mapping in |substitutions|.""" |
| 52 result = [] |
| 53 groups = SUBST_RE.split(plistlib.writePlistToString(plist)) |
| 54 # SUBST_RE defines two patterns, so given a string, SUBST_RE.split will |
| 55 # return a list whose position correspond to an unmatched fragment, a |
| 56 # match identifier or a match modifier (may be None as it is optional). |
| 57 # Enumerate over the value and check the index modulo 3 to figure out |
| 58 # the category of the value. |
| 59 for i, value in enumerate(groups): |
| 60 if i % 3 == 0: |
| 61 result.append(value) |
| 62 elif i % 3 == 1: |
| 63 if value in substitutions: |
| 64 result.append(substitutions[value]) |
| 65 else: |
| 66 result.append('${%s}' % value) |
| 67 elif i % 3 == 2: |
| 68 value, modifier = result[-1], value |
| 69 # Apply the modifier if defined (otherwise the value will be None). At |
| 70 # the beginning Xcode only supported :identifier that replaced all the |
| 71 # characters that are invalid by an hyphen, however, this is an invalid |
| 72 # character in a bundle identifier which is a reversed domain name and |
| 73 # so :rfc1034identifier was created that instead uses an underscore. |
| 74 if modifier == 'identifier': |
| 75 value = IDENT_RE.sub('-', value) |
| 76 elif modifier == 'rfc1034identifier': |
| 77 value = IDENT_RE.sub('_', value) |
| 78 result[-1] = value |
| 79 return CleanPlist(plistlib.readPlistFromString(''.join(result))) |
| 80 |
| 81 |
| 82 def CleanPlist(plist): |
| 83 """Removes all key of |plist| with unexpanded variables substitution.""" |
| 84 key_to_remove = [] |
| 85 for key in plist: |
| 86 value = plist[key] |
| 87 data = plistlib.writePlistToString(value) |
| 88 if SUBST_RE.search(data): |
| 89 key_to_remove.append(key) |
| 90 for key in key_to_remove: |
| 91 del plist[key] |
| 92 return plist |
| 93 |
| 94 |
| 95 def LoadPList(path): |
| 96 """Loads Plist at |path| and returns it as a dictionary.""" |
| 97 fd, name = tempfile.mkstemp() |
| 98 try: |
| 99 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) |
| 100 with os.fdopen(fd, 'r') as f: |
| 101 return plistlib.readPlist(f) |
| 102 finally: |
| 103 os.unlink(name) |
| 104 |
| 105 |
| 106 def SavePList(path, data): |
| 107 """Saves |data| as a Plist to |path| in binary1 format.""" |
| 108 fd, name = tempfile.mkstemp() |
| 109 try: |
| 110 with os.fdopen(fd, 'w') as f: |
| 111 plistlib.writePlist(data, f) |
| 112 subprocess.check_call(['plutil', '-convert', 'binary1', '-o', path, name]) |
| 113 finally: |
| 114 os.unlink(name) |
| 115 |
| 116 |
| 117 def main(): |
| 118 parser = ArgumentParser( |
| 119 description='A script to generate iOS application Info.plist.', |
| 120 fromfile_prefix_chars='@') |
| 121 parser.add_argument('-o', '--output', required=True, |
| 122 help='Path to output plist file.') |
| 123 parser.add_argument('-s', '--subst', action='append', default=[], |
| 124 help='Substitution rule in the format "key=value".') |
| 125 parser.add_argument('path', nargs="+", help='Path to input plist files.') |
| 126 args = parser.parse_args() |
| 127 |
| 128 substitutions = {} |
| 129 for subst in args.subst: |
| 130 key, value = subst.split('=', 1) |
| 131 substitutions[key] = value |
| 132 |
| 133 data = {} |
| 134 for path in args.path: |
| 135 data = Merge(data, LoadPList(path)) |
| 136 |
| 137 data = Interpolate(data, substitutions) |
| 138 SavePList(args.output, data) |
| 139 return 0 |
| 140 |
| 141 |
| 142 if __name__ == '__main__': |
| 143 sys.exit(main()) |
| OLD | NEW |