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:]))
|
---|