20b3a4865fec575a956b39497faad0feeee61398
[platform/upstream/nodejs.git] / tools / 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 json
13 import os
14 import plistlib
15 import re
16 import shutil
17 import string
18 import subprocess
19 import sys
20
21
22 def main(args):
23   executor = MacTool()
24   exit_code = executor.Dispatch(args)
25   if exit_code is not None:
26     sys.exit(exit_code)
27
28
29 class MacTool(object):
30   """This class performs all the Mac tooling steps. The methods can either be
31   executed directly, or dispatched from an argument list."""
32
33   def Dispatch(self, args):
34     """Dispatches a string command to a method."""
35     if len(args) < 1:
36       raise Exception("Not enough arguments")
37
38     method = "Exec%s" % self._CommandifyName(args[0])
39     return getattr(self, method)(*args[1:])
40
41   def _CommandifyName(self, name_string):
42     """Transforms a tool name like copy-info-plist to CopyInfoPlist"""
43     return name_string.title().replace('-', '')
44
45   def ExecCopyBundleResource(self, source, dest):
46     """Copies a resource file to the bundle/Resources directory, performing any
47     necessary compilation on each resource."""
48     extension = os.path.splitext(source)[1].lower()
49     if os.path.isdir(source):
50       # Copy tree.
51       # TODO(thakis): This copies file attributes like mtime, while the
52       # single-file branch below doesn't. This should probably be changed to
53       # be consistent with the single-file branch.
54       if os.path.exists(dest):
55         shutil.rmtree(dest)
56       shutil.copytree(source, dest)
57     elif extension == '.xib':
58       return self._CopyXIBFile(source, dest)
59     elif extension == '.storyboard':
60       return self._CopyXIBFile(source, dest)
61     elif extension == '.strings':
62       self._CopyStringsFile(source, dest)
63     else:
64       shutil.copy(source, dest)
65
66   def _CopyXIBFile(self, source, dest):
67     """Compiles a XIB file with ibtool into a binary plist in the bundle."""
68
69     # ibtool sometimes crashes with relative paths. See crbug.com/314728.
70     base = os.path.dirname(os.path.realpath(__file__))
71     if os.path.relpath(source):
72       source = os.path.join(base, source)
73     if os.path.relpath(dest):
74       dest = os.path.join(base, dest)
75
76     args = ['xcrun', 'ibtool', '--errors', '--warnings', '--notices',
77         '--output-format', 'human-readable-text', '--compile', dest, source]
78     ibtool_section_re = re.compile(r'/\*.*\*/')
79     ibtool_re = re.compile(r'.*note:.*is clipping its content')
80     ibtoolout = subprocess.Popen(args, stdout=subprocess.PIPE)
81     current_section_header = None
82     for line in ibtoolout.stdout:
83       if ibtool_section_re.match(line):
84         current_section_header = line
85       elif not ibtool_re.match(line):
86         if current_section_header:
87           sys.stdout.write(current_section_header)
88           current_section_header = None
89         sys.stdout.write(line)
90     return ibtoolout.returncode
91
92   def _CopyStringsFile(self, source, dest):
93     """Copies a .strings file using iconv to reconvert the input into UTF-16."""
94     input_code = self._DetectInputEncoding(source) or "UTF-8"
95
96     # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call
97     # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints
98     #     CFPropertyListCreateFromXMLData(): Old-style plist parser: missing
99     #     semicolon in dictionary.
100     # on invalid files. Do the same kind of validation.
101     import CoreFoundation
102     s = open(source, 'rb').read()
103     d = CoreFoundation.CFDataCreate(None, s, len(s))
104     _, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None)
105     if error:
106       return
107
108     fp = open(dest, 'wb')
109     fp.write(s.decode(input_code).encode('UTF-16'))
110     fp.close()
111
112   def _DetectInputEncoding(self, file_name):
113     """Reads the first few bytes from file_name and tries to guess the text
114     encoding. Returns None as a guess if it can't detect it."""
115     fp = open(file_name, 'rb')
116     try:
117       header = fp.read(3)
118     except e:
119       fp.close()
120       return None
121     fp.close()
122     if header.startswith("\xFE\xFF"):
123       return "UTF-16"
124     elif header.startswith("\xFF\xFE"):
125       return "UTF-16"
126     elif header.startswith("\xEF\xBB\xBF"):
127       return "UTF-8"
128     else:
129       return None
130
131   def ExecCopyInfoPlist(self, source, dest, *keys):
132     """Copies the |source| Info.plist to the destination directory |dest|."""
133     # Read the source Info.plist into memory.
134     fd = open(source, 'r')
135     lines = fd.read()
136     fd.close()
137
138     # Insert synthesized key/value pairs (e.g. BuildMachineOSBuild).
139     plist = plistlib.readPlistFromString(lines)
140     if keys:
141       plist = dict(plist.items() + json.loads(keys[0]).items())
142     lines = plistlib.writePlistToString(plist)
143
144     # Go through all the environment variables and replace them as variables in
145     # the file.
146     IDENT_RE = re.compile('[/\s]')
147     for key in os.environ:
148       if key.startswith('_'):
149         continue
150       evar = '${%s}' % key
151       evalue = os.environ[key]
152       lines = string.replace(lines, evar, evalue)
153
154       # Xcode supports various suffices on environment variables, which are
155       # all undocumented. :rfc1034identifier is used in the standard project
156       # template these days, and :identifier was used earlier. They are used to
157       # convert non-url characters into things that look like valid urls --
158       # except that the replacement character for :identifier, '_' isn't valid
159       # in a URL either -- oops, hence :rfc1034identifier was born.
160       evar = '${%s:identifier}' % key
161       evalue = IDENT_RE.sub('_', os.environ[key])
162       lines = string.replace(lines, evar, evalue)
163
164       evar = '${%s:rfc1034identifier}' % key
165       evalue = IDENT_RE.sub('-', os.environ[key])
166       lines = string.replace(lines, evar, evalue)
167
168     # Remove any keys with values that haven't been replaced.
169     lines = lines.split('\n')
170     for i in range(len(lines)):
171       if lines[i].strip().startswith("<string>${"):
172         lines[i] = None
173         lines[i - 1] = None
174     lines = '\n'.join(filter(lambda x: x is not None, lines))
175
176     # Write out the file with variables replaced.
177     fd = open(dest, 'w')
178     fd.write(lines)
179     fd.close()
180
181     # Now write out PkgInfo file now that the Info.plist file has been
182     # "compiled".
183     self._WritePkgInfo(dest)
184
185   def _WritePkgInfo(self, info_plist):
186     """This writes the PkgInfo file from the data stored in Info.plist."""
187     plist = plistlib.readPlist(info_plist)
188     if not plist:
189       return
190
191     # Only create PkgInfo for executable types.
192     package_type = plist['CFBundlePackageType']
193     if package_type != 'APPL':
194       return
195
196     # The format of PkgInfo is eight characters, representing the bundle type
197     # and bundle signature, each four characters. If that is missing, four
198     # '?' characters are used instead.
199     signature_code = plist.get('CFBundleSignature', '????')
200     if len(signature_code) != 4:  # Wrong length resets everything, too.
201       signature_code = '?' * 4
202
203     dest = os.path.join(os.path.dirname(info_plist), 'PkgInfo')
204     fp = open(dest, 'w')
205     fp.write('%s%s' % (package_type, signature_code))
206     fp.close()
207
208   def ExecFlock(self, lockfile, *cmd_list):
209     """Emulates the most basic behavior of Linux's flock(1)."""
210     # Rely on exception handling to report errors.
211     fd = os.open(lockfile, os.O_RDONLY|os.O_NOCTTY|os.O_CREAT, 0o666)
212     fcntl.flock(fd, fcntl.LOCK_EX)
213     return subprocess.call(cmd_list)
214
215   def ExecFilterLibtool(self, *cmd_list):
216     """Calls libtool and filters out '/path/to/libtool: file: foo.o has no
217     symbols'."""
218     libtool_re = re.compile(r'^.*libtool: file: .* has no symbols$')
219     libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE)
220     _, err = libtoolout.communicate()
221     for line in err.splitlines():
222       if not libtool_re.match(line):
223         print >>sys.stderr, line
224     return libtoolout.returncode
225
226   def ExecPackageFramework(self, framework, version):
227     """Takes a path to Something.framework and the Current version of that and
228     sets up all the symlinks."""
229     # Find the name of the binary based on the part before the ".framework".
230     binary = os.path.basename(framework).split('.')[0]
231
232     CURRENT = 'Current'
233     RESOURCES = 'Resources'
234     VERSIONS = 'Versions'
235
236     if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)):
237       # Binary-less frameworks don't seem to contain symlinks (see e.g.
238       # chromium's out/Debug/org.chromium.Chromium.manifest/ bundle).
239       return
240
241     # Move into the framework directory to set the symlinks correctly.
242     pwd = os.getcwd()
243     os.chdir(framework)
244
245     # Set up the Current version.
246     self._Relink(version, os.path.join(VERSIONS, CURRENT))
247
248     # Set up the root symlinks.
249     self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary)
250     self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES)
251
252     # Back to where we were before!
253     os.chdir(pwd)
254
255   def _Relink(self, dest, link):
256     """Creates a symlink to |dest| named |link|. If |link| already exists,
257     it is overwritten."""
258     if os.path.lexists(link):
259       os.remove(link)
260     os.symlink(dest, link)
261
262
263 if __name__ == '__main__':
264   sys.exit(main(sys.argv[1:]))