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