[6a3a178] | 1 | #!/usr/bin/env python
|
---|
| 2 | # Copyright (c) 2012 Google Inc. All rights reserved.
|
---|
| 3 | # Use of this source code is governed by a BSD-style license that can be
|
---|
| 4 | # found in the LICENSE file.
|
---|
| 5 |
|
---|
| 6 | """Utility functions to perform Xcode-style build steps.
|
---|
| 7 |
|
---|
| 8 | These functions are executed via gyp-mac-tool when using the Makefile generator.
|
---|
| 9 | """
|
---|
| 10 |
|
---|
| 11 | from __future__ import print_function
|
---|
| 12 |
|
---|
| 13 | import fcntl
|
---|
| 14 | import fnmatch
|
---|
| 15 | import glob
|
---|
| 16 | import json
|
---|
| 17 | import os
|
---|
| 18 | import plistlib
|
---|
| 19 | import re
|
---|
| 20 | import shutil
|
---|
| 21 | import struct
|
---|
| 22 | import subprocess
|
---|
| 23 | import sys
|
---|
| 24 | import tempfile
|
---|
| 25 |
|
---|
| 26 | PY3 = bytes != str
|
---|
| 27 |
|
---|
| 28 |
|
---|
| 29 | def main(args):
|
---|
| 30 | executor = MacTool()
|
---|
| 31 | exit_code = executor.Dispatch(args)
|
---|
| 32 | if exit_code is not None:
|
---|
| 33 | sys.exit(exit_code)
|
---|
| 34 |
|
---|
| 35 |
|
---|
| 36 | class MacTool(object):
|
---|
| 37 | """This class performs all the Mac tooling steps. The methods can either be
|
---|
| 38 | executed directly, or dispatched from an argument list."""
|
---|
| 39 |
|
---|
| 40 | def Dispatch(self, args):
|
---|
| 41 | """Dispatches a string command to a method."""
|
---|
| 42 | if len(args) < 1:
|
---|
| 43 | raise Exception("Not enough arguments")
|
---|
| 44 |
|
---|
| 45 | method = "Exec%s" % self._CommandifyName(args[0])
|
---|
| 46 | return getattr(self, method)(*args[1:])
|
---|
| 47 |
|
---|
| 48 | def _CommandifyName(self, name_string):
|
---|
| 49 | """Transforms a tool name like copy-info-plist to CopyInfoPlist"""
|
---|
| 50 | return name_string.title().replace("-", "")
|
---|
| 51 |
|
---|
| 52 | def ExecCopyBundleResource(self, source, dest, convert_to_binary):
|
---|
| 53 | """Copies a resource file to the bundle/Resources directory, performing any
|
---|
| 54 | necessary compilation on each resource."""
|
---|
| 55 | convert_to_binary = convert_to_binary == "True"
|
---|
| 56 | extension = os.path.splitext(source)[1].lower()
|
---|
| 57 | if os.path.isdir(source):
|
---|
| 58 | # Copy tree.
|
---|
| 59 | # TODO(thakis): This copies file attributes like mtime, while the
|
---|
| 60 | # single-file branch below doesn't. This should probably be changed to
|
---|
| 61 | # be consistent with the single-file branch.
|
---|
| 62 | if os.path.exists(dest):
|
---|
| 63 | shutil.rmtree(dest)
|
---|
| 64 | shutil.copytree(source, dest)
|
---|
| 65 | elif extension == ".xib":
|
---|
| 66 | return self._CopyXIBFile(source, dest)
|
---|
| 67 | elif extension == ".storyboard":
|
---|
| 68 | return self._CopyXIBFile(source, dest)
|
---|
| 69 | elif extension == ".strings" and not convert_to_binary:
|
---|
| 70 | self._CopyStringsFile(source, dest)
|
---|
| 71 | else:
|
---|
| 72 | if os.path.exists(dest):
|
---|
| 73 | os.unlink(dest)
|
---|
| 74 | shutil.copy(source, dest)
|
---|
| 75 |
|
---|
| 76 | if convert_to_binary and extension in (".plist", ".strings"):
|
---|
| 77 | self._ConvertToBinary(dest)
|
---|
| 78 |
|
---|
| 79 | def _CopyXIBFile(self, source, dest):
|
---|
| 80 | """Compiles a XIB file with ibtool into a binary plist in the bundle."""
|
---|
| 81 |
|
---|
| 82 | # ibtool sometimes crashes with relative paths. See crbug.com/314728.
|
---|
| 83 | base = os.path.dirname(os.path.realpath(__file__))
|
---|
| 84 | if os.path.relpath(source):
|
---|
| 85 | source = os.path.join(base, source)
|
---|
| 86 | if os.path.relpath(dest):
|
---|
| 87 | dest = os.path.join(base, dest)
|
---|
| 88 |
|
---|
| 89 | args = ["xcrun", "ibtool", "--errors", "--warnings", "--notices"]
|
---|
| 90 |
|
---|
| 91 | if os.environ["XCODE_VERSION_ACTUAL"] > "0700":
|
---|
| 92 | args.extend(["--auto-activate-custom-fonts"])
|
---|
| 93 | if "IPHONEOS_DEPLOYMENT_TARGET" in os.environ:
|
---|
| 94 | args.extend(
|
---|
| 95 | [
|
---|
| 96 | "--target-device",
|
---|
| 97 | "iphone",
|
---|
| 98 | "--target-device",
|
---|
| 99 | "ipad",
|
---|
| 100 | "--minimum-deployment-target",
|
---|
| 101 | os.environ["IPHONEOS_DEPLOYMENT_TARGET"],
|
---|
| 102 | ]
|
---|
| 103 | )
|
---|
| 104 | else:
|
---|
| 105 | args.extend(
|
---|
| 106 | [
|
---|
| 107 | "--target-device",
|
---|
| 108 | "mac",
|
---|
| 109 | "--minimum-deployment-target",
|
---|
| 110 | os.environ["MACOSX_DEPLOYMENT_TARGET"],
|
---|
| 111 | ]
|
---|
| 112 | )
|
---|
| 113 |
|
---|
| 114 | args.extend(
|
---|
| 115 | ["--output-format", "human-readable-text", "--compile", dest, source]
|
---|
| 116 | )
|
---|
| 117 |
|
---|
| 118 | ibtool_section_re = re.compile(r"/\*.*\*/")
|
---|
| 119 | ibtool_re = re.compile(r".*note:.*is clipping its content")
|
---|
| 120 | try:
|
---|
| 121 | stdout = subprocess.check_output(args)
|
---|
| 122 | except subprocess.CalledProcessError as e:
|
---|
| 123 | print(e.output)
|
---|
| 124 | raise
|
---|
| 125 | current_section_header = None
|
---|
| 126 | for line in stdout.splitlines():
|
---|
| 127 | if ibtool_section_re.match(line):
|
---|
| 128 | current_section_header = line
|
---|
| 129 | elif not ibtool_re.match(line):
|
---|
| 130 | if current_section_header:
|
---|
| 131 | print(current_section_header)
|
---|
| 132 | current_section_header = None
|
---|
| 133 | print(line)
|
---|
| 134 | return 0
|
---|
| 135 |
|
---|
| 136 | def _ConvertToBinary(self, dest):
|
---|
| 137 | subprocess.check_call(
|
---|
| 138 | ["xcrun", "plutil", "-convert", "binary1", "-o", dest, dest]
|
---|
| 139 | )
|
---|
| 140 |
|
---|
| 141 | def _CopyStringsFile(self, source, dest):
|
---|
| 142 | """Copies a .strings file using iconv to reconvert the input into UTF-16."""
|
---|
| 143 | input_code = self._DetectInputEncoding(source) or "UTF-8"
|
---|
| 144 |
|
---|
| 145 | # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call
|
---|
| 146 | # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints
|
---|
| 147 | # CFPropertyListCreateFromXMLData(): Old-style plist parser: missing
|
---|
| 148 | # semicolon in dictionary.
|
---|
| 149 | # on invalid files. Do the same kind of validation.
|
---|
| 150 | import CoreFoundation
|
---|
| 151 |
|
---|
| 152 | with open(source, "rb") as in_file:
|
---|
| 153 | s = in_file.read()
|
---|
| 154 | d = CoreFoundation.CFDataCreate(None, s, len(s))
|
---|
| 155 | _, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None)
|
---|
| 156 | if error:
|
---|
| 157 | return
|
---|
| 158 |
|
---|
| 159 | with open(dest, "wb") as fp:
|
---|
| 160 | fp.write(s.decode(input_code).encode("UTF-16"))
|
---|
| 161 |
|
---|
| 162 | def _DetectInputEncoding(self, file_name):
|
---|
| 163 | """Reads the first few bytes from file_name and tries to guess the text
|
---|
| 164 | encoding. Returns None as a guess if it can't detect it."""
|
---|
| 165 | with open(file_name, "rb") as fp:
|
---|
| 166 | try:
|
---|
| 167 | header = fp.read(3)
|
---|
| 168 | except Exception:
|
---|
| 169 | return None
|
---|
| 170 | if header.startswith(b"\xFE\xFF"):
|
---|
| 171 | return "UTF-16"
|
---|
| 172 | elif header.startswith(b"\xFF\xFE"):
|
---|
| 173 | return "UTF-16"
|
---|
| 174 | elif header.startswith(b"\xEF\xBB\xBF"):
|
---|
| 175 | return "UTF-8"
|
---|
| 176 | else:
|
---|
| 177 | return None
|
---|
| 178 |
|
---|
| 179 | def ExecCopyInfoPlist(self, source, dest, convert_to_binary, *keys):
|
---|
| 180 | """Copies the |source| Info.plist to the destination directory |dest|."""
|
---|
| 181 | # Read the source Info.plist into memory.
|
---|
| 182 | with open(source, "r") as fd:
|
---|
| 183 | lines = fd.read()
|
---|
| 184 |
|
---|
| 185 | # Insert synthesized key/value pairs (e.g. BuildMachineOSBuild).
|
---|
| 186 | plist = plistlib.readPlistFromString(lines)
|
---|
| 187 | if keys:
|
---|
| 188 | plist.update(json.loads(keys[0]))
|
---|
| 189 | lines = plistlib.writePlistToString(plist)
|
---|
| 190 |
|
---|
| 191 | # Go through all the environment variables and replace them as variables in
|
---|
| 192 | # the file.
|
---|
| 193 | IDENT_RE = re.compile(r"[_/\s]")
|
---|
| 194 | for key in os.environ:
|
---|
| 195 | if key.startswith("_"):
|
---|
| 196 | continue
|
---|
| 197 | evar = "${%s}" % key
|
---|
| 198 | evalue = os.environ[key]
|
---|
| 199 | lines = lines.replace(lines, evar, evalue)
|
---|
| 200 |
|
---|
| 201 | # Xcode supports various suffices on environment variables, which are
|
---|
| 202 | # all undocumented. :rfc1034identifier is used in the standard project
|
---|
| 203 | # template these days, and :identifier was used earlier. They are used to
|
---|
| 204 | # convert non-url characters into things that look like valid urls --
|
---|
| 205 | # except that the replacement character for :identifier, '_' isn't valid
|
---|
| 206 | # in a URL either -- oops, hence :rfc1034identifier was born.
|
---|
| 207 | evar = "${%s:identifier}" % key
|
---|
| 208 | evalue = IDENT_RE.sub("_", os.environ[key])
|
---|
| 209 | lines = lines.replace(lines, evar, evalue)
|
---|
| 210 |
|
---|
| 211 | evar = "${%s:rfc1034identifier}" % key
|
---|
| 212 | evalue = IDENT_RE.sub("-", os.environ[key])
|
---|
| 213 | lines = lines.replace(lines, evar, evalue)
|
---|
| 214 |
|
---|
| 215 | # Remove any keys with values that haven't been replaced.
|
---|
| 216 | lines = lines.splitlines()
|
---|
| 217 | for i in range(len(lines)):
|
---|
| 218 | if lines[i].strip().startswith("<string>${"):
|
---|
| 219 | lines[i] = None
|
---|
| 220 | lines[i - 1] = None
|
---|
| 221 | lines = "\n".join(line for line in lines if line is not None)
|
---|
| 222 |
|
---|
| 223 | # Write out the file with variables replaced.
|
---|
| 224 | with open(dest, "w") as fd:
|
---|
| 225 | fd.write(lines)
|
---|
| 226 |
|
---|
| 227 | # Now write out PkgInfo file now that the Info.plist file has been
|
---|
| 228 | # "compiled".
|
---|
| 229 | self._WritePkgInfo(dest)
|
---|
| 230 |
|
---|
| 231 | if convert_to_binary == "True":
|
---|
| 232 | self._ConvertToBinary(dest)
|
---|
| 233 |
|
---|
| 234 | def _WritePkgInfo(self, info_plist):
|
---|
| 235 | """This writes the PkgInfo file from the data stored in Info.plist."""
|
---|
| 236 | plist = plistlib.readPlist(info_plist)
|
---|
| 237 | if not plist:
|
---|
| 238 | return
|
---|
| 239 |
|
---|
| 240 | # Only create PkgInfo for executable types.
|
---|
| 241 | package_type = plist["CFBundlePackageType"]
|
---|
| 242 | if package_type != "APPL":
|
---|
| 243 | return
|
---|
| 244 |
|
---|
| 245 | # The format of PkgInfo is eight characters, representing the bundle type
|
---|
| 246 | # and bundle signature, each four characters. If that is missing, four
|
---|
| 247 | # '?' characters are used instead.
|
---|
| 248 | signature_code = plist.get("CFBundleSignature", "????")
|
---|
| 249 | if len(signature_code) != 4: # Wrong length resets everything, too.
|
---|
| 250 | signature_code = "?" * 4
|
---|
| 251 |
|
---|
| 252 | dest = os.path.join(os.path.dirname(info_plist), "PkgInfo")
|
---|
| 253 | with open(dest, "w") as fp:
|
---|
| 254 | fp.write("%s%s" % (package_type, signature_code))
|
---|
| 255 |
|
---|
| 256 | def ExecFlock(self, lockfile, *cmd_list):
|
---|
| 257 | """Emulates the most basic behavior of Linux's flock(1)."""
|
---|
| 258 | # Rely on exception handling to report errors.
|
---|
| 259 | fd = os.open(lockfile, os.O_RDONLY | os.O_NOCTTY | os.O_CREAT, 0o666)
|
---|
| 260 | fcntl.flock(fd, fcntl.LOCK_EX)
|
---|
| 261 | return subprocess.call(cmd_list)
|
---|
| 262 |
|
---|
| 263 | def ExecFilterLibtool(self, *cmd_list):
|
---|
| 264 | """Calls libtool and filters out '/path/to/libtool: file: foo.o has no
|
---|
| 265 | symbols'."""
|
---|
| 266 | libtool_re = re.compile(
|
---|
| 267 | r"^.*libtool: (?:for architecture: \S* )?" r"file: .* has no symbols$"
|
---|
| 268 | )
|
---|
| 269 | libtool_re5 = re.compile(
|
---|
| 270 | r"^.*libtool: warning for library: "
|
---|
| 271 | + r".* the table of contents is empty "
|
---|
| 272 | + r"\(no object file members in the library define global symbols\)$"
|
---|
| 273 | )
|
---|
| 274 | env = os.environ.copy()
|
---|
| 275 | # Ref:
|
---|
| 276 | # http://www.opensource.apple.com/source/cctools/cctools-809/misc/libtool.c
|
---|
| 277 | # The problem with this flag is that it resets the file mtime on the file to
|
---|
| 278 | # epoch=0, e.g. 1970-1-1 or 1969-12-31 depending on timezone.
|
---|
| 279 | env["ZERO_AR_DATE"] = "1"
|
---|
| 280 | libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE, env=env)
|
---|
| 281 | _, err = libtoolout.communicate()
|
---|
| 282 | if PY3:
|
---|
| 283 | err = err.decode("utf-8")
|
---|
| 284 | for line in err.splitlines():
|
---|
| 285 | if not libtool_re.match(line) and not libtool_re5.match(line):
|
---|
| 286 | print(line, file=sys.stderr)
|
---|
| 287 | # Unconditionally touch the output .a file on the command line if present
|
---|
| 288 | # and the command succeeded. A bit hacky.
|
---|
| 289 | if not libtoolout.returncode:
|
---|
| 290 | for i in range(len(cmd_list) - 1):
|
---|
| 291 | if cmd_list[i] == "-o" and cmd_list[i + 1].endswith(".a"):
|
---|
| 292 | os.utime(cmd_list[i + 1], None)
|
---|
| 293 | break
|
---|
| 294 | return libtoolout.returncode
|
---|
| 295 |
|
---|
| 296 | def ExecPackageIosFramework(self, framework):
|
---|
| 297 | # Find the name of the binary based on the part before the ".framework".
|
---|
| 298 | binary = os.path.basename(framework).split(".")[0]
|
---|
| 299 | module_path = os.path.join(framework, "Modules")
|
---|
| 300 | if not os.path.exists(module_path):
|
---|
| 301 | os.mkdir(module_path)
|
---|
| 302 | module_template = (
|
---|
| 303 | "framework module %s {\n"
|
---|
| 304 | ' umbrella header "%s.h"\n'
|
---|
| 305 | "\n"
|
---|
| 306 | " export *\n"
|
---|
| 307 | " module * { export * }\n"
|
---|
| 308 | "}\n" % (binary, binary)
|
---|
| 309 | )
|
---|
| 310 |
|
---|
| 311 | with open(os.path.join(module_path, "module.modulemap"), "w") as module_file:
|
---|
| 312 | module_file.write(module_template)
|
---|
| 313 |
|
---|
| 314 | def ExecPackageFramework(self, framework, version):
|
---|
| 315 | """Takes a path to Something.framework and the Current version of that and
|
---|
| 316 | sets up all the symlinks."""
|
---|
| 317 | # Find the name of the binary based on the part before the ".framework".
|
---|
| 318 | binary = os.path.basename(framework).split(".")[0]
|
---|
| 319 |
|
---|
| 320 | CURRENT = "Current"
|
---|
| 321 | RESOURCES = "Resources"
|
---|
| 322 | VERSIONS = "Versions"
|
---|
| 323 |
|
---|
| 324 | if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)):
|
---|
| 325 | # Binary-less frameworks don't seem to contain symlinks (see e.g.
|
---|
| 326 | # chromium's out/Debug/org.chromium.Chromium.manifest/ bundle).
|
---|
| 327 | return
|
---|
| 328 |
|
---|
| 329 | # Move into the framework directory to set the symlinks correctly.
|
---|
| 330 | pwd = os.getcwd()
|
---|
| 331 | os.chdir(framework)
|
---|
| 332 |
|
---|
| 333 | # Set up the Current version.
|
---|
| 334 | self._Relink(version, os.path.join(VERSIONS, CURRENT))
|
---|
| 335 |
|
---|
| 336 | # Set up the root symlinks.
|
---|
| 337 | self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary)
|
---|
| 338 | self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES)
|
---|
| 339 |
|
---|
| 340 | # Back to where we were before!
|
---|
| 341 | os.chdir(pwd)
|
---|
| 342 |
|
---|
| 343 | def _Relink(self, dest, link):
|
---|
| 344 | """Creates a symlink to |dest| named |link|. If |link| already exists,
|
---|
| 345 | it is overwritten."""
|
---|
| 346 | if os.path.lexists(link):
|
---|
| 347 | os.remove(link)
|
---|
| 348 | os.symlink(dest, link)
|
---|
| 349 |
|
---|
| 350 | def ExecCompileIosFrameworkHeaderMap(self, out, framework, *all_headers):
|
---|
| 351 | framework_name = os.path.basename(framework).split(".")[0]
|
---|
| 352 | all_headers = [os.path.abspath(header) for header in all_headers]
|
---|
| 353 | filelist = {}
|
---|
| 354 | for header in all_headers:
|
---|
| 355 | filename = os.path.basename(header)
|
---|
| 356 | filelist[filename] = header
|
---|
| 357 | filelist[os.path.join(framework_name, filename)] = header
|
---|
| 358 | WriteHmap(out, filelist)
|
---|
| 359 |
|
---|
| 360 | def ExecCopyIosFrameworkHeaders(self, framework, *copy_headers):
|
---|
| 361 | header_path = os.path.join(framework, "Headers")
|
---|
| 362 | if not os.path.exists(header_path):
|
---|
| 363 | os.makedirs(header_path)
|
---|
| 364 | for header in copy_headers:
|
---|
| 365 | shutil.copy(header, os.path.join(header_path, os.path.basename(header)))
|
---|
| 366 |
|
---|
| 367 | def ExecCompileXcassets(self, keys, *inputs):
|
---|
| 368 | """Compiles multiple .xcassets files into a single .car file.
|
---|
| 369 |
|
---|
| 370 | This invokes 'actool' to compile all the inputs .xcassets files. The
|
---|
| 371 | |keys| arguments is a json-encoded dictionary of extra arguments to
|
---|
| 372 | pass to 'actool' when the asset catalogs contains an application icon
|
---|
| 373 | or a launch image.
|
---|
| 374 |
|
---|
| 375 | Note that 'actool' does not create the Assets.car file if the asset
|
---|
| 376 | catalogs does not contains imageset.
|
---|
| 377 | """
|
---|
| 378 | command_line = [
|
---|
| 379 | "xcrun",
|
---|
| 380 | "actool",
|
---|
| 381 | "--output-format",
|
---|
| 382 | "human-readable-text",
|
---|
| 383 | "--compress-pngs",
|
---|
| 384 | "--notices",
|
---|
| 385 | "--warnings",
|
---|
| 386 | "--errors",
|
---|
| 387 | ]
|
---|
| 388 | is_iphone_target = "IPHONEOS_DEPLOYMENT_TARGET" in os.environ
|
---|
| 389 | if is_iphone_target:
|
---|
| 390 | platform = os.environ["CONFIGURATION"].split("-")[-1]
|
---|
| 391 | if platform not in ("iphoneos", "iphonesimulator"):
|
---|
| 392 | platform = "iphonesimulator"
|
---|
| 393 | command_line.extend(
|
---|
| 394 | [
|
---|
| 395 | "--platform",
|
---|
| 396 | platform,
|
---|
| 397 | "--target-device",
|
---|
| 398 | "iphone",
|
---|
| 399 | "--target-device",
|
---|
| 400 | "ipad",
|
---|
| 401 | "--minimum-deployment-target",
|
---|
| 402 | os.environ["IPHONEOS_DEPLOYMENT_TARGET"],
|
---|
| 403 | "--compile",
|
---|
| 404 | os.path.abspath(os.environ["CONTENTS_FOLDER_PATH"]),
|
---|
| 405 | ]
|
---|
| 406 | )
|
---|
| 407 | else:
|
---|
| 408 | command_line.extend(
|
---|
| 409 | [
|
---|
| 410 | "--platform",
|
---|
| 411 | "macosx",
|
---|
| 412 | "--target-device",
|
---|
| 413 | "mac",
|
---|
| 414 | "--minimum-deployment-target",
|
---|
| 415 | os.environ["MACOSX_DEPLOYMENT_TARGET"],
|
---|
| 416 | "--compile",
|
---|
| 417 | os.path.abspath(os.environ["UNLOCALIZED_RESOURCES_FOLDER_PATH"]),
|
---|
| 418 | ]
|
---|
| 419 | )
|
---|
| 420 | if keys:
|
---|
| 421 | keys = json.loads(keys)
|
---|
| 422 | for key, value in keys.items():
|
---|
| 423 | arg_name = "--" + key
|
---|
| 424 | if isinstance(value, bool):
|
---|
| 425 | if value:
|
---|
| 426 | command_line.append(arg_name)
|
---|
| 427 | elif isinstance(value, list):
|
---|
| 428 | for v in value:
|
---|
| 429 | command_line.append(arg_name)
|
---|
| 430 | command_line.append(str(v))
|
---|
| 431 | else:
|
---|
| 432 | command_line.append(arg_name)
|
---|
| 433 | command_line.append(str(value))
|
---|
| 434 | # Note: actool crashes if inputs path are relative, so use os.path.abspath
|
---|
| 435 | # to get absolute path name for inputs.
|
---|
| 436 | command_line.extend(map(os.path.abspath, inputs))
|
---|
| 437 | subprocess.check_call(command_line)
|
---|
| 438 |
|
---|
| 439 | def ExecMergeInfoPlist(self, output, *inputs):
|
---|
| 440 | """Merge multiple .plist files into a single .plist file."""
|
---|
| 441 | merged_plist = {}
|
---|
| 442 | for path in inputs:
|
---|
| 443 | plist = self._LoadPlistMaybeBinary(path)
|
---|
| 444 | self._MergePlist(merged_plist, plist)
|
---|
| 445 | plistlib.writePlist(merged_plist, output)
|
---|
| 446 |
|
---|
| 447 | def ExecCodeSignBundle(self, key, entitlements, provisioning, path, preserve):
|
---|
| 448 | """Code sign a bundle.
|
---|
| 449 |
|
---|
| 450 | This function tries to code sign an iOS bundle, following the same
|
---|
| 451 | algorithm as Xcode:
|
---|
| 452 | 1. pick the provisioning profile that best match the bundle identifier,
|
---|
| 453 | and copy it into the bundle as embedded.mobileprovision,
|
---|
| 454 | 2. copy Entitlements.plist from user or SDK next to the bundle,
|
---|
| 455 | 3. code sign the bundle.
|
---|
| 456 | """
|
---|
| 457 | substitutions, overrides = self._InstallProvisioningProfile(
|
---|
| 458 | provisioning, self._GetCFBundleIdentifier()
|
---|
| 459 | )
|
---|
| 460 | entitlements_path = self._InstallEntitlements(
|
---|
| 461 | entitlements, substitutions, overrides
|
---|
| 462 | )
|
---|
| 463 |
|
---|
| 464 | args = ["codesign", "--force", "--sign", key]
|
---|
| 465 | if preserve == "True":
|
---|
| 466 | args.extend(["--deep", "--preserve-metadata=identifier,entitlements"])
|
---|
| 467 | else:
|
---|
| 468 | args.extend(["--entitlements", entitlements_path])
|
---|
| 469 | args.extend(["--timestamp=none", path])
|
---|
| 470 | subprocess.check_call(args)
|
---|
| 471 |
|
---|
| 472 | def _InstallProvisioningProfile(self, profile, bundle_identifier):
|
---|
| 473 | """Installs embedded.mobileprovision into the bundle.
|
---|
| 474 |
|
---|
| 475 | Args:
|
---|
| 476 | profile: string, optional, short name of the .mobileprovision file
|
---|
| 477 | to use, if empty or the file is missing, the best file installed
|
---|
| 478 | will be used
|
---|
| 479 | bundle_identifier: string, value of CFBundleIdentifier from Info.plist
|
---|
| 480 |
|
---|
| 481 | Returns:
|
---|
| 482 | A tuple containing two dictionary: variables substitutions and values
|
---|
| 483 | to overrides when generating the entitlements file.
|
---|
| 484 | """
|
---|
| 485 | source_path, provisioning_data, team_id = self._FindProvisioningProfile(
|
---|
| 486 | profile, bundle_identifier
|
---|
| 487 | )
|
---|
| 488 | target_path = os.path.join(
|
---|
| 489 | os.environ["BUILT_PRODUCTS_DIR"],
|
---|
| 490 | os.environ["CONTENTS_FOLDER_PATH"],
|
---|
| 491 | "embedded.mobileprovision",
|
---|
| 492 | )
|
---|
| 493 | shutil.copy2(source_path, target_path)
|
---|
| 494 | substitutions = self._GetSubstitutions(bundle_identifier, team_id + ".")
|
---|
| 495 | return substitutions, provisioning_data["Entitlements"]
|
---|
| 496 |
|
---|
| 497 | def _FindProvisioningProfile(self, profile, bundle_identifier):
|
---|
| 498 | """Finds the .mobileprovision file to use for signing the bundle.
|
---|
| 499 |
|
---|
| 500 | Checks all the installed provisioning profiles (or if the user specified
|
---|
| 501 | the PROVISIONING_PROFILE variable, only consult it) and select the most
|
---|
| 502 | specific that correspond to the bundle identifier.
|
---|
| 503 |
|
---|
| 504 | Args:
|
---|
| 505 | profile: string, optional, short name of the .mobileprovision file
|
---|
| 506 | to use, if empty or the file is missing, the best file installed
|
---|
| 507 | will be used
|
---|
| 508 | bundle_identifier: string, value of CFBundleIdentifier from Info.plist
|
---|
| 509 |
|
---|
| 510 | Returns:
|
---|
| 511 | A tuple of the path to the selected provisioning profile, the data of
|
---|
| 512 | the embedded plist in the provisioning profile and the team identifier
|
---|
| 513 | to use for code signing.
|
---|
| 514 |
|
---|
| 515 | Raises:
|
---|
| 516 | SystemExit: if no .mobileprovision can be used to sign the bundle.
|
---|
| 517 | """
|
---|
| 518 | profiles_dir = os.path.join(
|
---|
| 519 | os.environ["HOME"], "Library", "MobileDevice", "Provisioning Profiles"
|
---|
| 520 | )
|
---|
| 521 | if not os.path.isdir(profiles_dir):
|
---|
| 522 | print(
|
---|
| 523 | "cannot find mobile provisioning for %s" % (bundle_identifier),
|
---|
| 524 | file=sys.stderr,
|
---|
| 525 | )
|
---|
| 526 | sys.exit(1)
|
---|
| 527 | provisioning_profiles = None
|
---|
| 528 | if profile:
|
---|
| 529 | profile_path = os.path.join(profiles_dir, profile + ".mobileprovision")
|
---|
| 530 | if os.path.exists(profile_path):
|
---|
| 531 | provisioning_profiles = [profile_path]
|
---|
| 532 | if not provisioning_profiles:
|
---|
| 533 | provisioning_profiles = glob.glob(
|
---|
| 534 | os.path.join(profiles_dir, "*.mobileprovision")
|
---|
| 535 | )
|
---|
| 536 | valid_provisioning_profiles = {}
|
---|
| 537 | for profile_path in provisioning_profiles:
|
---|
| 538 | profile_data = self._LoadProvisioningProfile(profile_path)
|
---|
| 539 | app_id_pattern = profile_data.get("Entitlements", {}).get(
|
---|
| 540 | "application-identifier", ""
|
---|
| 541 | )
|
---|
| 542 | for team_identifier in profile_data.get("TeamIdentifier", []):
|
---|
| 543 | app_id = "%s.%s" % (team_identifier, bundle_identifier)
|
---|
| 544 | if fnmatch.fnmatch(app_id, app_id_pattern):
|
---|
| 545 | valid_provisioning_profiles[app_id_pattern] = (
|
---|
| 546 | profile_path,
|
---|
| 547 | profile_data,
|
---|
| 548 | team_identifier,
|
---|
| 549 | )
|
---|
| 550 | if not valid_provisioning_profiles:
|
---|
| 551 | print(
|
---|
| 552 | "cannot find mobile provisioning for %s" % (bundle_identifier),
|
---|
| 553 | file=sys.stderr,
|
---|
| 554 | )
|
---|
| 555 | sys.exit(1)
|
---|
| 556 | # If the user has multiple provisioning profiles installed that can be
|
---|
| 557 | # used for ${bundle_identifier}, pick the most specific one (ie. the
|
---|
| 558 | # provisioning profile whose pattern is the longest).
|
---|
| 559 | selected_key = max(valid_provisioning_profiles, key=lambda v: len(v))
|
---|
| 560 | return valid_provisioning_profiles[selected_key]
|
---|
| 561 |
|
---|
| 562 | def _LoadProvisioningProfile(self, profile_path):
|
---|
| 563 | """Extracts the plist embedded in a provisioning profile.
|
---|
| 564 |
|
---|
| 565 | Args:
|
---|
| 566 | profile_path: string, path to the .mobileprovision file
|
---|
| 567 |
|
---|
| 568 | Returns:
|
---|
| 569 | Content of the plist embedded in the provisioning profile as a dictionary.
|
---|
| 570 | """
|
---|
| 571 | with tempfile.NamedTemporaryFile() as temp:
|
---|
| 572 | subprocess.check_call(
|
---|
| 573 | ["security", "cms", "-D", "-i", profile_path, "-o", temp.name]
|
---|
| 574 | )
|
---|
| 575 | return self._LoadPlistMaybeBinary(temp.name)
|
---|
| 576 |
|
---|
| 577 | def _MergePlist(self, merged_plist, plist):
|
---|
| 578 | """Merge |plist| into |merged_plist|."""
|
---|
| 579 | for key, value in plist.items():
|
---|
| 580 | if isinstance(value, dict):
|
---|
| 581 | merged_value = merged_plist.get(key, {})
|
---|
| 582 | if isinstance(merged_value, dict):
|
---|
| 583 | self._MergePlist(merged_value, value)
|
---|
| 584 | merged_plist[key] = merged_value
|
---|
| 585 | else:
|
---|
| 586 | merged_plist[key] = value
|
---|
| 587 | else:
|
---|
| 588 | merged_plist[key] = value
|
---|
| 589 |
|
---|
| 590 | def _LoadPlistMaybeBinary(self, plist_path):
|
---|
| 591 | """Loads into a memory a plist possibly encoded in binary format.
|
---|
| 592 |
|
---|
| 593 | This is a wrapper around plistlib.readPlist that tries to convert the
|
---|
| 594 | plist to the XML format if it can't be parsed (assuming that it is in
|
---|
| 595 | the binary format).
|
---|
| 596 |
|
---|
| 597 | Args:
|
---|
| 598 | plist_path: string, path to a plist file, in XML or binary format
|
---|
| 599 |
|
---|
| 600 | Returns:
|
---|
| 601 | Content of the plist as a dictionary.
|
---|
| 602 | """
|
---|
| 603 | try:
|
---|
| 604 | # First, try to read the file using plistlib that only supports XML,
|
---|
| 605 | # and if an exception is raised, convert a temporary copy to XML and
|
---|
| 606 | # load that copy.
|
---|
| 607 | return plistlib.readPlist(plist_path)
|
---|
| 608 | except Exception:
|
---|
| 609 | pass
|
---|
| 610 | with tempfile.NamedTemporaryFile() as temp:
|
---|
| 611 | shutil.copy2(plist_path, temp.name)
|
---|
| 612 | subprocess.check_call(["plutil", "-convert", "xml1", temp.name])
|
---|
| 613 | return plistlib.readPlist(temp.name)
|
---|
| 614 |
|
---|
| 615 | def _GetSubstitutions(self, bundle_identifier, app_identifier_prefix):
|
---|
| 616 | """Constructs a dictionary of variable substitutions for Entitlements.plist.
|
---|
| 617 |
|
---|
| 618 | Args:
|
---|
| 619 | bundle_identifier: string, value of CFBundleIdentifier from Info.plist
|
---|
| 620 | app_identifier_prefix: string, value for AppIdentifierPrefix
|
---|
| 621 |
|
---|
| 622 | Returns:
|
---|
| 623 | Dictionary of substitutions to apply when generating Entitlements.plist.
|
---|
| 624 | """
|
---|
| 625 | return {
|
---|
| 626 | "CFBundleIdentifier": bundle_identifier,
|
---|
| 627 | "AppIdentifierPrefix": app_identifier_prefix,
|
---|
| 628 | }
|
---|
| 629 |
|
---|
| 630 | def _GetCFBundleIdentifier(self):
|
---|
| 631 | """Extracts CFBundleIdentifier value from Info.plist in the bundle.
|
---|
| 632 |
|
---|
| 633 | Returns:
|
---|
| 634 | Value of CFBundleIdentifier in the Info.plist located in the bundle.
|
---|
| 635 | """
|
---|
| 636 | info_plist_path = os.path.join(
|
---|
| 637 | os.environ["TARGET_BUILD_DIR"], os.environ["INFOPLIST_PATH"]
|
---|
| 638 | )
|
---|
| 639 | info_plist_data = self._LoadPlistMaybeBinary(info_plist_path)
|
---|
| 640 | return info_plist_data["CFBundleIdentifier"]
|
---|
| 641 |
|
---|
| 642 | def _InstallEntitlements(self, entitlements, substitutions, overrides):
|
---|
| 643 | """Generates and install the ${BundleName}.xcent entitlements file.
|
---|
| 644 |
|
---|
| 645 | Expands variables "$(variable)" pattern in the source entitlements file,
|
---|
| 646 | add extra entitlements defined in the .mobileprovision file and the copy
|
---|
| 647 | the generated plist to "${BundlePath}.xcent".
|
---|
| 648 |
|
---|
| 649 | Args:
|
---|
| 650 | entitlements: string, optional, path to the Entitlements.plist template
|
---|
| 651 | to use, defaults to "${SDKROOT}/Entitlements.plist"
|
---|
| 652 | substitutions: dictionary, variable substitutions
|
---|
| 653 | overrides: dictionary, values to add to the entitlements
|
---|
| 654 |
|
---|
| 655 | Returns:
|
---|
| 656 | Path to the generated entitlements file.
|
---|
| 657 | """
|
---|
| 658 | source_path = entitlements
|
---|
| 659 | target_path = os.path.join(
|
---|
| 660 | os.environ["BUILT_PRODUCTS_DIR"], os.environ["PRODUCT_NAME"] + ".xcent"
|
---|
| 661 | )
|
---|
| 662 | if not source_path:
|
---|
| 663 | source_path = os.path.join(os.environ["SDKROOT"], "Entitlements.plist")
|
---|
| 664 | shutil.copy2(source_path, target_path)
|
---|
| 665 | data = self._LoadPlistMaybeBinary(target_path)
|
---|
| 666 | data = self._ExpandVariables(data, substitutions)
|
---|
| 667 | if overrides:
|
---|
| 668 | for key in overrides:
|
---|
| 669 | if key not in data:
|
---|
| 670 | data[key] = overrides[key]
|
---|
| 671 | plistlib.writePlist(data, target_path)
|
---|
| 672 | return target_path
|
---|
| 673 |
|
---|
| 674 | def _ExpandVariables(self, data, substitutions):
|
---|
| 675 | """Expands variables "$(variable)" in data.
|
---|
| 676 |
|
---|
| 677 | Args:
|
---|
| 678 | data: object, can be either string, list or dictionary
|
---|
| 679 | substitutions: dictionary, variable substitutions to perform
|
---|
| 680 |
|
---|
| 681 | Returns:
|
---|
| 682 | Copy of data where each references to "$(variable)" has been replaced
|
---|
| 683 | by the corresponding value found in substitutions, or left intact if
|
---|
| 684 | the key was not found.
|
---|
| 685 | """
|
---|
| 686 | if isinstance(data, str):
|
---|
| 687 | for key, value in substitutions.items():
|
---|
| 688 | data = data.replace("$(%s)" % key, value)
|
---|
| 689 | return data
|
---|
| 690 | if isinstance(data, list):
|
---|
| 691 | return [self._ExpandVariables(v, substitutions) for v in data]
|
---|
| 692 | if isinstance(data, dict):
|
---|
| 693 | return {k: self._ExpandVariables(data[k], substitutions) for k in data}
|
---|
| 694 | return data
|
---|
| 695 |
|
---|
| 696 |
|
---|
| 697 | def NextGreaterPowerOf2(x):
|
---|
| 698 | return 2 ** (x).bit_length()
|
---|
| 699 |
|
---|
| 700 |
|
---|
| 701 | def WriteHmap(output_name, filelist):
|
---|
| 702 | """Generates a header map based on |filelist|.
|
---|
| 703 |
|
---|
| 704 | Per Mark Mentovai:
|
---|
| 705 | A header map is structured essentially as a hash table, keyed by names used
|
---|
| 706 | in #includes, and providing pathnames to the actual files.
|
---|
| 707 |
|
---|
| 708 | The implementation below and the comment above comes from inspecting:
|
---|
| 709 | http://www.opensource.apple.com/source/distcc/distcc-2503/distcc_dist/include_server/headermap.py?txt
|
---|
| 710 | while also looking at the implementation in clang in:
|
---|
| 711 | https://llvm.org/svn/llvm-project/cfe/trunk/lib/Lex/HeaderMap.cpp
|
---|
| 712 | """
|
---|
| 713 | magic = 1751998832
|
---|
| 714 | version = 1
|
---|
| 715 | _reserved = 0
|
---|
| 716 | count = len(filelist)
|
---|
| 717 | capacity = NextGreaterPowerOf2(count)
|
---|
| 718 | strings_offset = 24 + (12 * capacity)
|
---|
| 719 | max_value_length = max(len(value) for value in filelist.values())
|
---|
| 720 |
|
---|
| 721 | out = open(output_name, "wb")
|
---|
| 722 | out.write(
|
---|
| 723 | struct.pack(
|
---|
| 724 | "<LHHLLLL",
|
---|
| 725 | magic,
|
---|
| 726 | version,
|
---|
| 727 | _reserved,
|
---|
| 728 | strings_offset,
|
---|
| 729 | count,
|
---|
| 730 | capacity,
|
---|
| 731 | max_value_length,
|
---|
| 732 | )
|
---|
| 733 | )
|
---|
| 734 |
|
---|
| 735 | # Create empty hashmap buckets.
|
---|
| 736 | buckets = [None] * capacity
|
---|
| 737 | for file, path in filelist.items():
|
---|
| 738 | key = 0
|
---|
| 739 | for c in file:
|
---|
| 740 | key += ord(c.lower()) * 13
|
---|
| 741 |
|
---|
| 742 | # Fill next empty bucket.
|
---|
| 743 | while buckets[key & capacity - 1] is not None:
|
---|
| 744 | key = key + 1
|
---|
| 745 | buckets[key & capacity - 1] = (file, path)
|
---|
| 746 |
|
---|
| 747 | next_offset = 1
|
---|
| 748 | for bucket in buckets:
|
---|
| 749 | if bucket is None:
|
---|
| 750 | out.write(struct.pack("<LLL", 0, 0, 0))
|
---|
| 751 | else:
|
---|
| 752 | (file, path) = bucket
|
---|
| 753 | key_offset = next_offset
|
---|
| 754 | prefix_offset = key_offset + len(file) + 1
|
---|
| 755 | suffix_offset = prefix_offset + len(os.path.dirname(path) + os.sep) + 1
|
---|
| 756 | next_offset = suffix_offset + len(os.path.basename(path)) + 1
|
---|
| 757 | out.write(struct.pack("<LLL", key_offset, prefix_offset, suffix_offset))
|
---|
| 758 |
|
---|
| 759 | # Pad byte since next offset starts at 1.
|
---|
| 760 | out.write(struct.pack("<x"))
|
---|
| 761 |
|
---|
| 762 | for bucket in buckets:
|
---|
| 763 | if bucket is not None:
|
---|
| 764 | (file, path) = bucket
|
---|
| 765 | out.write(struct.pack("<%ds" % len(file), file))
|
---|
| 766 | out.write(struct.pack("<s", "\0"))
|
---|
| 767 | base = os.path.dirname(path) + os.sep
|
---|
| 768 | out.write(struct.pack("<%ds" % len(base), base))
|
---|
| 769 | out.write(struct.pack("<s", "\0"))
|
---|
| 770 | path = os.path.basename(path)
|
---|
| 771 | out.write(struct.pack("<%ds" % len(path), path))
|
---|
| 772 | out.write(struct.pack("<s", "\0"))
|
---|
| 773 |
|
---|
| 774 |
|
---|
| 775 | if __name__ == "__main__":
|
---|
| 776 | sys.exit(main(sys.argv[1:]))
|
---|