xref: /aosp_15_r20/external/cronet/build/apple/plist_util.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import argparse
6import codecs
7import plistlib
8import os
9import re
10import subprocess
11import sys
12import tempfile
13import shlex
14
15# Xcode substitutes variables like ${PRODUCT_NAME} or $(PRODUCT_NAME) when
16# compiling Info.plist. It also supports supports modifiers like :identifier
17# or :rfc1034identifier. SUBSTITUTION_REGEXP_LIST is a list of regular
18# expressions matching a variable substitution pattern with an optional
19# modifier, while INVALID_CHARACTER_REGEXP matches all characters that are
20# not valid in an "identifier" value (used when applying the modifier).
21INVALID_CHARACTER_REGEXP = re.compile(r'[_/\s]')
22SUBSTITUTION_REGEXP_LIST = (
23    re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}'),
24    re.compile(r'\$\((?P<id>[^}]*?)(?P<modifier>:[^}]*)?\)'),
25)
26
27
28class SubstitutionError(Exception):
29  def __init__(self, key):
30    super(SubstitutionError, self).__init__()
31    self.key = key
32
33  def __str__(self):
34    return "SubstitutionError: {}".format(self.key)
35
36
37def InterpolateString(value, substitutions):
38  """Interpolates variable references into |value| using |substitutions|.
39
40  Inputs:
41    value: a string
42    substitutions: a mapping of variable names to values
43
44  Returns:
45    A new string with all variables references ${VARIABLES} replaced by their
46    value in |substitutions|. Raises SubstitutionError if a variable has no
47    substitution.
48  """
49
50  def repl(match):
51    variable = match.group('id')
52    if variable not in substitutions:
53      raise SubstitutionError(variable)
54    # Some values need to be identifier and thus the variables references may
55    # contains :modifier attributes to indicate how they should be converted
56    # to identifiers ("identifier" replaces all invalid characters by '_' and
57    # "rfc1034identifier" replaces them by "-" to make valid URI too).
58    modifier = match.group('modifier')
59    if modifier == ':identifier':
60      return INVALID_CHARACTER_REGEXP.sub('_', substitutions[variable])
61    elif modifier == ':rfc1034identifier':
62      return INVALID_CHARACTER_REGEXP.sub('-', substitutions[variable])
63    else:
64      return substitutions[variable]
65
66  for substitution_regexp in SUBSTITUTION_REGEXP_LIST:
67    value = substitution_regexp.sub(repl, value)
68  return value
69
70
71def Interpolate(value, substitutions):
72  """Interpolates variable references into |value| using |substitutions|.
73
74  Inputs:
75    value: a value, can be a dictionary, list, string or other
76    substitutions: a mapping of variable names to values
77
78  Returns:
79    A new value with all variables references ${VARIABLES} replaced by their
80    value in |substitutions|. Raises SubstitutionError if a variable has no
81    substitution.
82  """
83  if isinstance(value, dict):
84    return {k: Interpolate(v, substitutions) for k, v in value.items()}
85  if isinstance(value, list):
86    return [Interpolate(v, substitutions) for v in value]
87  if isinstance(value, str):
88    return InterpolateString(value, substitutions)
89  return value
90
91
92def LoadPList(path):
93  """Loads Plist at |path| and returns it as a dictionary."""
94  with open(path, 'rb') as f:
95    return plistlib.load(f)
96
97
98def SavePList(path, format, data):
99  """Saves |data| as a Plist to |path| in the specified |format|."""
100  # The open() call does not replace the destination file but updates it
101  # in place, so if more than one hardlink points to destination all of them
102  # will be modified. This is not what is expected, so delete destination file
103  # if it does exist.
104  try:
105    os.unlink(path)
106  except FileNotFoundError:
107    pass
108  with open(path, 'wb') as f:
109    plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML}
110    plistlib.dump(data, f, fmt=plist_format[format])
111
112
113def MergePList(plist1, plist2):
114  """Merges |plist1| with |plist2| recursively.
115
116  Creates a new dictionary representing a Property List (.plist) files by
117  merging the two dictionary |plist1| and |plist2| recursively (only for
118  dictionary values). List value will be concatenated.
119
120  Args:
121    plist1: a dictionary representing a Property List (.plist) file
122    plist2: a dictionary representing a Property List (.plist) file
123
124  Returns:
125    A new dictionary representing a Property List (.plist) file by merging
126    |plist1| with |plist2|. If any value is a dictionary, they are merged
127    recursively, otherwise |plist2| value is used. If values are list, they
128    are concatenated.
129  """
130  result = plist1.copy()
131  for key, value in plist2.items():
132    if isinstance(value, dict):
133      old_value = result.get(key)
134      if isinstance(old_value, dict):
135        value = MergePList(old_value, value)
136    if isinstance(value, list):
137      value = plist1.get(key, []) + plist2.get(key, [])
138    result[key] = value
139  return result
140
141
142class Action(object):
143  """Class implementing one action supported by the script."""
144
145  @classmethod
146  def Register(cls, subparsers):
147    parser = subparsers.add_parser(cls.name, help=cls.help)
148    parser.set_defaults(func=cls._Execute)
149    cls._Register(parser)
150
151
152class MergeAction(Action):
153  """Class to merge multiple plist files."""
154
155  name = 'merge'
156  help = 'merge multiple plist files'
157
158  @staticmethod
159  def _Register(parser):
160    parser.add_argument('-o',
161                        '--output',
162                        required=True,
163                        help='path to the output plist file')
164    parser.add_argument('-f',
165                        '--format',
166                        required=True,
167                        choices=('xml1', 'binary1'),
168                        help='format of the plist file to generate')
169    parser.add_argument(
170        '-x',
171        '--xcode-version',
172        help='version of Xcode, ignored (can be used to force rebuild)')
173    parser.add_argument('path', nargs="+", help='path to plist files to merge')
174
175  @staticmethod
176  def _Execute(args):
177    data = {}
178    for filename in args.path:
179      data = MergePList(data, LoadPList(filename))
180    SavePList(args.output, args.format, data)
181
182
183class SubstituteAction(Action):
184  """Class implementing the variable substitution in a plist file."""
185
186  name = 'substitute'
187  help = 'perform pattern substitution in a plist file'
188
189  @staticmethod
190  def _Register(parser):
191    parser.add_argument('-o',
192                        '--output',
193                        required=True,
194                        help='path to the output plist file')
195    parser.add_argument('-t',
196                        '--template',
197                        required=True,
198                        help='path to the template file')
199    parser.add_argument('-s',
200                        '--substitution',
201                        action='append',
202                        default=[],
203                        help='substitution rule in the format key=value')
204    parser.add_argument('-f',
205                        '--format',
206                        required=True,
207                        choices=('xml1', 'binary1'),
208                        help='format of the plist file to generate')
209    parser.add_argument(
210        '-x',
211        '--xcode-version',
212        help='version of Xcode, ignored (can be used to force rebuild)')
213
214  @staticmethod
215  def _Execute(args):
216    substitutions = {}
217    for substitution in args.substitution:
218      key, value = substitution.split('=', 1)
219      substitutions[key] = value
220    data = Interpolate(LoadPList(args.template), substitutions)
221    SavePList(args.output, args.format, data)
222
223
224def Main():
225  parser = argparse.ArgumentParser(description='manipulate plist files')
226  subparsers = parser.add_subparsers()
227
228  for action in [MergeAction, SubstituteAction]:
229    action.Register(subparsers)
230
231  args = parser.parse_args()
232  args.func(args)
233
234
235if __name__ == '__main__':
236  sys.exit(Main())
237