# 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} when compiling Info.plist. # It also supports supports modifiers like :identifier or :rfc1034identifier. # SUBST_RE matches a variable substitution pattern with an optional modifier, # while IDENT_RE matches all characters that are not valid in an "identifier" # value (used when applying the modifier). SUBST_RE = re.compile(r'\$\{(?P[^}]*?)(?P:[^}]*)?\}') IDENT_RE = re.compile(r'[/\s]') class ArgumentParser(argparse.ArgumentParser): """Subclass of argparse.ArgumentParser to work with GN response files. GN response file writes all the arguments on a single line and assumes that the python script uses shlext.split() to extract them. Since the default ArgumentParser expects a single argument per line, we need to provide a subclass to have the correct support for @{{response_file_name}}. """ def convert_arg_line_to_args(self, arg_line): return shlex.split(arg_line) def InterpolateList(values, substitutions): """Interpolates variable references into |value| using |substitutions|. Inputs: values: a list of values substitutions: a mapping of variable names to values Returns: A new list of values with all variables references ${VARIABLE} replaced by their value in |substitutions| or None if any of the variable has no subsitution. """ result = [] for value in values: interpolated = InterpolateValue(value, substitutions) if interpolated is None: return None result.append(interpolated) return result 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| or None if any of the variable has no substitution. """ result = value for match in reversed(list(SUBST_RE.finditer(value))): variable = match.group('id') if variable not in substitutions: return None # 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': interpolated = IDENT_RE.sub('-', substitutions[variable]) elif modifier == 'rfc1034identifier': interpolated = IDENT_RE.sub('_', substitutions[variable]) else: interpolated = substitutions[variable] result = result[:match.start()] + interpolated + result[match.end():] return result def InterpolateValue(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| or None if any of the variable has no substitution. """ if isinstance(value, dict): return Interpolate(value, substitutions) if isinstance(value, list): return InterpolateList(value, substitutions) if isinstance(value, str): return InterpolateString(value, substitutions) return value def Interpolate(plist, substitutions): """Interpolates variable references into |value| using |substitutions|. Inputs: plist: a dictionary representing a Property List (.plist) file substitutions: a mapping of variable names to values Returns: A new plist with all variables references ${VARIABLES} replaced by their value in |substitutions|. All values that contains references with no substitutions will be removed and the corresponding key will be cleared from the plist (not recursively). """ result = {} for key in plist: value = InterpolateValue(plist[key], substitutions) if value is not None: result[key] = value return result 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, data): """Saves |data| as a Plist to |path| in binary1 format.""" fd, name = tempfile.mkstemp() try: with os.fdopen(fd, 'w') as f: plistlib.writePlist(data, f) subprocess.check_call(['plutil', '-convert', 'binary1', '-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). 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 not isinstance(plist1, dict) or not isinstance(plist2, dict): if plist2 is not None: return plist2 else: return plist1 result = {} for key in set(plist1) | set(plist2): if key in plist2: value = plist2[key] else: value = plist1[key] if isinstance(value, dict): value = MergePList(plist1.get(key, None), plist2.get(key, None)) result[key] = value return result def main(): parser = ArgumentParser( description='A script to generate iOS application Info.plist.', fromfile_prefix_chars='@') parser.add_argument('-o', '--output', required=True, help='Path to output plist file.') parser.add_argument('-s', '--subst', action='append', default=[], help='Substitution rule in the format "key=value".') parser.add_argument('path', nargs="+", help='Path to input plist files.') args = parser.parse_args() substitutions = {} for subst in args.subst: key, value = subst.split('=', 1) substitutions[key] = value data = {} for filename in args.path: data = MergePList(data, LoadPList(filename)) data = Interpolate(data, substitutions) SavePList(args.output, data) return 0 if __name__ == '__main__': sys.exit(main())