普通文本  |  210行  |  6.92 KB

# 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<id>[^}]*?)(?P<modifier>:[^}]*)?\}')
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, format, data):
  """Saves |data| as a Plist to |path| in the specified |format|."""
  fd, name = tempfile.mkstemp()
  try:
    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).

  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('-f', '--format', required=True,
                      help='Plist format (e.g. binary1, xml1) to output.')
  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, args.format, data)
  return 0

if __name__ == '__main__':
  sys.exit(main())