1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
|
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import codecs
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.items()}
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."""
with open(path, 'rb') as f:
return plistlib.load(f)
def SavePList(path, format, data):
"""Saves |data| as a Plist to |path| in the specified |format|."""
# The open() call does not replace the destination file but updates 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.
try:
os.unlink(path)
except FileNotFoundError:
pass
with open(path, 'wb') as f:
plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML}
plistlib.dump(data, f, fmt=plist_format[format])
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.items():
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'),
help='format of the plist file to generate')
parser.add_argument(
'-x',
'--xcode-version',
help='version of Xcode, ignored (can be used to force rebuild)')
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'),
help='format of the plist file to generate')
parser.add_argument(
'-x',
'--xcode-version',
help='version of Xcode, ignored (can be used to force rebuild)')
@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())
|