1 # Copyright (c) 2012 Google Inc. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 from __future__ import with_statement
16 # A minimal memoizing decorator. It'll blow up if the args aren't immutable,
17 # among other "problems".
18 class memoize(object):
19 def __init__(self, func):
22 def __call__(self, *args):
24 return self.cache[args]
26 result = self.func(*args)
27 self.cache[args] = result
31 class GypError(Exception):
32 """Error class representing an error, which is to be presented
33 to the user. The main entry point will catch and display this.
38 def ExceptionAppend(e, msg):
39 """Append a message to the given exception's message."""
42 elif len(e.args) == 1:
43 e.args = (str(e.args[0]) + ' ' + msg,)
45 e.args = (str(e.args[0]) + ' ' + msg,) + e.args[1:]
48 def FindQualifiedTargets(target, qualified_list):
50 Given a list of qualified targets, return the qualified targets for the
53 return [t for t in qualified_list if ParseQualifiedTarget(t)[1] == target]
56 def ParseQualifiedTarget(target):
57 # Splits a qualified target into a build file, target name and toolset.
59 # NOTE: rsplit is used to disambiguate the Windows drive letter separator.
60 target_split = target.rsplit(':', 1)
61 if len(target_split) == 2:
62 [build_file, target] = target_split
66 target_split = target.rsplit('#', 1)
67 if len(target_split) == 2:
68 [target, toolset] = target_split
72 return [build_file, target, toolset]
75 def ResolveTarget(build_file, target, toolset):
76 # This function resolves a target into a canonical form:
77 # - a fully defined build file, either absolute or relative to the current
82 # build_file is the file relative to which 'target' is defined.
83 # target is the qualified target.
84 # toolset is the default toolset for that target.
85 [parsed_build_file, target, parsed_toolset] = ParseQualifiedTarget(target)
89 # If a relative path, parsed_build_file is relative to the directory
90 # containing build_file. If build_file is not in the current directory,
91 # parsed_build_file is not a usable path as-is. Resolve it by
92 # interpreting it as relative to build_file. If parsed_build_file is
93 # absolute, it is usable as a path regardless of the current directory,
94 # and os.path.join will return it as-is.
95 build_file = os.path.normpath(os.path.join(os.path.dirname(build_file),
97 # Further (to handle cases like ../cwd), make it relative to cwd)
98 if not os.path.isabs(build_file):
99 build_file = RelativePath(build_file, '.')
101 build_file = parsed_build_file
104 toolset = parsed_toolset
106 return [build_file, target, toolset]
109 def BuildFile(fully_qualified_target):
110 # Extracts the build file from the fully qualified target.
111 return ParseQualifiedTarget(fully_qualified_target)[0]
114 def GetEnvironFallback(var_list, default):
115 """Look up a key in the environment, with fallback to secondary keys
116 and finally falling back to a default value."""
118 if var in os.environ:
119 return os.environ[var]
123 def QualifiedTarget(build_file, target, toolset):
124 # "Qualified" means the file that a target was defined in and the target
125 # name, separated by a colon, suffixed by a # and the toolset name:
126 # /path/to/file.gyp:target_name#toolset
127 fully_qualified = build_file + ':' + target
129 fully_qualified = fully_qualified + '#' + toolset
130 return fully_qualified
134 def RelativePath(path, relative_to, follow_path_symlink=True):
135 # Assuming both |path| and |relative_to| are relative to the current
136 # directory, returns a relative path that identifies path relative to
138 # If |follow_symlink_path| is true (default) and |path| is a symlink, then
139 # this method returns a path to the real file represented by |path|. If it is
140 # false, this method returns a path to the symlink. If |path| is not a
141 # symlink, this option has no effect.
143 # Convert to normalized (and therefore absolute paths).
144 if follow_path_symlink:
145 path = os.path.realpath(path)
147 path = os.path.abspath(path)
148 relative_to = os.path.realpath(relative_to)
150 # On Windows, we can't create a relative path to a different drive, so just
151 # use the absolute path.
152 if sys.platform == 'win32':
153 if (os.path.splitdrive(path)[0].lower() !=
154 os.path.splitdrive(relative_to)[0].lower()):
157 # Split the paths into components.
158 path_split = path.split(os.path.sep)
159 relative_to_split = relative_to.split(os.path.sep)
161 # Determine how much of the prefix the two paths share.
162 prefix_len = len(os.path.commonprefix([path_split, relative_to_split]))
164 # Put enough ".." components to back up out of relative_to to the common
165 # prefix, and then append the part of path_split after the common prefix.
166 relative_split = [os.path.pardir] * (len(relative_to_split) - prefix_len) + \
167 path_split[prefix_len:]
169 if len(relative_split) == 0:
170 # The paths were the same.
173 # Turn it back into a string and we're done.
174 return os.path.join(*relative_split)
178 def InvertRelativePath(path, toplevel_dir=None):
179 """Given a path like foo/bar that is relative to toplevel_dir, return
180 the inverse relative path back to the toplevel_dir.
182 E.g. os.path.normpath(os.path.join(path, InvertRelativePath(path)))
183 should always produce the empty string, unless the path contains symlinks.
187 toplevel_dir = '.' if toplevel_dir is None else toplevel_dir
188 return RelativePath(toplevel_dir, os.path.join(toplevel_dir, path))
191 def FixIfRelativePath(path, relative_to):
192 # Like RelativePath but returns |path| unchanged if it is absolute.
193 if os.path.isabs(path):
195 return RelativePath(path, relative_to)
198 def UnrelativePath(path, relative_to):
199 # Assuming that |relative_to| is relative to the current directory, and |path|
200 # is a path relative to the dirname of |relative_to|, returns a path that
201 # identifies |path| relative to the current directory.
202 rel_dir = os.path.dirname(relative_to)
203 return os.path.normpath(os.path.join(rel_dir, path))
206 # re objects used by EncodePOSIXShellArgument. See IEEE 1003.1 XCU.2.2 at
207 # http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02
208 # and the documentation for various shells.
210 # _quote is a pattern that should match any argument that needs to be quoted
211 # with double-quotes by EncodePOSIXShellArgument. It matches the following
212 # characters appearing anywhere in an argument:
213 # \t, \n, space parameter separators
215 # $ expansions (quoted to always expand within one argument)
216 # % called out by IEEE 1003.1 XCU.2.2
219 # (, ) subshell execution
220 # *, ?, [ pathname expansion
221 # ; command delimiter
222 # <, >, | redirection
224 # {, } brace expansion (bash)
226 # It also matches the empty string, because "" (or '') is the only way to
227 # represent an empty string literal argument to a POSIX shell.
229 # This does not match the characters in _escape, because those need to be
230 # backslash-escaped regardless of whether they appear in a double-quoted
232 _quote = re.compile('[\t\n #$%&\'()*;<=>?[{|}~]|^$')
234 # _escape is a pattern that should match any character that needs to be
235 # escaped with a backslash, whether or not the argument matched the _quote
236 # pattern. _escape is used with re.sub to backslash anything in _escape's
237 # first match group, hence the (parentheses) in the regular expression.
239 # _escape matches the following characters appearing anywhere in an argument:
240 # " to prevent POSIX shells from interpreting this character for quoting
241 # \ to prevent POSIX shells from interpreting this character for escaping
242 # ` to prevent POSIX shells from interpreting this character for command
244 # Missing from this list is $, because the desired behavior of
245 # EncodePOSIXShellArgument is to permit parameter (variable) expansion.
247 # Also missing from this list is !, which bash will interpret as the history
248 # expansion character when history is enabled. bash does not enable history
249 # by default in non-interactive shells, so this is not thought to be a problem.
250 # ! was omitted from this list because bash interprets "\!" as a literal string
251 # including the backslash character (avoiding history expansion but retaining
252 # the backslash), which would not be correct for argument encoding. Handling
253 # this case properly would also be problematic because bash allows the history
254 # character to be changed with the histchars shell variable. Fortunately,
255 # as history is not enabled in non-interactive shells and
256 # EncodePOSIXShellArgument is only expected to encode for non-interactive
257 # shells, there is no room for error here by ignoring !.
258 _escape = re.compile(r'(["\\`])')
260 def EncodePOSIXShellArgument(argument):
261 """Encodes |argument| suitably for consumption by POSIX shells.
263 argument may be quoted and escaped as necessary to ensure that POSIX shells
264 treat the returned value as a literal representing the argument passed to
265 this function. Parameter (variable) expansions beginning with $ are allowed
266 to remain intact without escaping the $, to allow the argument to contain
267 references to variables to be expanded by the shell.
270 if not isinstance(argument, str):
271 argument = str(argument)
273 if _quote.search(argument):
278 encoded = quote + re.sub(_escape, r'\\\1', argument) + quote
283 def EncodePOSIXShellList(list):
284 """Encodes |list| suitably for consumption by POSIX shells.
286 Returns EncodePOSIXShellArgument for each item in list, and joins them
287 together using the space character as an argument separator.
290 encoded_arguments = []
291 for argument in list:
292 encoded_arguments.append(EncodePOSIXShellArgument(argument))
293 return ' '.join(encoded_arguments)
296 def DeepDependencyTargets(target_dicts, roots):
297 """Returns the recursive list of target dependencies."""
303 # Skip if visited already.
304 if r in dependencies:
309 spec = target_dicts[r]
310 pending.update(set(spec.get('dependencies', [])))
311 pending.update(set(spec.get('dependencies_original', [])))
312 return list(dependencies - set(roots))
315 def BuildFileTargets(target_list, build_file):
316 """From a target_list, returns the subset from the specified build_file.
318 return [p for p in target_list if BuildFile(p) == build_file]
321 def AllTargets(target_list, target_dicts, build_file):
322 """Returns all targets (direct and dependencies) for the specified build_file.
324 bftargets = BuildFileTargets(target_list, build_file)
325 deptargets = DeepDependencyTargets(target_dicts, bftargets)
326 return bftargets + deptargets
329 def WriteOnDiff(filename):
330 """Write to a file only if the new contents differ.
333 filename: name of the file to potentially write to.
335 A file like object which will write to temporary file and only overwrite
336 the target if it differs (on close).
339 class Writer(object):
340 """Wrapper around file which only covers the target if it differs."""
342 # Pick temporary file.
343 tmp_fd, self.tmp_path = tempfile.mkstemp(
345 prefix=os.path.split(filename)[1] + '.gyp.',
346 dir=os.path.split(filename)[0])
348 self.tmp_file = os.fdopen(tmp_fd, 'wb')
350 # Don't leave turds behind.
351 os.unlink(self.tmp_path)
354 def __getattr__(self, attrname):
355 # Delegate everything else to self.tmp_file
356 return getattr(self.tmp_file, attrname)
361 self.tmp_file.close()
362 # Determine if different.
365 same = filecmp.cmp(self.tmp_path, filename, False)
367 if e.errno != errno.ENOENT:
371 # The new file is identical to the old one, just get rid of the new
373 os.unlink(self.tmp_path)
375 # The new file is different from the old one, or there is no old one.
376 # Rename the new file to the permanent name.
378 # tempfile.mkstemp uses an overly restrictive mode, resulting in a
379 # file that can only be read by the owner, regardless of the umask.
380 # There's no reason to not respect the umask here, which means that
381 # an extra hoop is required to fetch it and reset the new file's mode.
383 # No way to get the umask without setting a new one? Set a safe one
384 # and then set it back to the old value.
385 umask = os.umask(077)
387 os.chmod(self.tmp_path, 0666 & ~umask)
388 if sys.platform == 'win32' and os.path.exists(filename):
389 # NOTE: on windows (but not cygwin) rename will not replace an
390 # existing file, so it must be preceded with a remove. Sadly there
391 # is no way to make the switch atomic.
393 os.rename(self.tmp_path, filename)
395 # Don't leave turds behind.
396 os.unlink(self.tmp_path)
402 def EnsureDirExists(path):
403 """Make sure the directory for |path| exists."""
405 os.makedirs(os.path.dirname(path))
410 def GetFlavor(params):
411 """Returns |params.flavor| if it's set, the system's default flavor else."""
418 if 'flavor' in params:
419 return params['flavor']
420 if sys.platform in flavors:
421 return flavors[sys.platform]
422 if sys.platform.startswith('sunos'):
424 if sys.platform.startswith('freebsd'):
426 if sys.platform.startswith('openbsd'):
428 if sys.platform.startswith('netbsd'):
430 if sys.platform.startswith('aix'):
436 def CopyTool(flavor, out_path):
437 """Finds (flock|mac|win)_tool.gyp in the gyp directory and copies it
439 # aix and solaris just need flock emulation. mac and win use more complicated
451 source_path = os.path.join(
452 os.path.dirname(os.path.abspath(__file__)), '%s_tool.py' % prefix)
453 with open(source_path) as source_file:
454 source = source_file.readlines()
456 # Add header and write it out.
457 tool_path = os.path.join(out_path, 'gyp-%s-tool' % prefix)
458 with open(tool_path, 'w') as tool_file:
460 ''.join([source[0], '# Generated by gyp. Do not edit.\n'] + source[1:]))
462 # Make file executable.
463 os.chmod(tool_path, 0755)
466 # From Alex Martelli,
467 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560
468 # ASPN: Python Cookbook: Remove duplicates from a sequence
469 # First comment, dated 2001/10/13.
470 # (Also in the printed Python Cookbook.)
472 def uniquer(seq, idfun=None):
479 if marker in seen: continue
485 # Based on http://code.activestate.com/recipes/576694/.
486 class OrderedSet(collections.MutableSet):
487 def __init__(self, iterable=None):
489 end += [None, end, end] # sentinel node for doubly linked list
490 self.map = {} # key --> [key, prev, next]
491 if iterable is not None:
497 def __contains__(self, key):
498 return key in self.map
501 if key not in self.map:
504 curr[2] = end[1] = self.map[key] = [key, curr, end]
506 def discard(self, key):
508 key, prev_item, next_item = self.map.pop(key)
509 prev_item[2] = next_item
510 next_item[1] = prev_item
515 while curr is not end:
519 def __reversed__(self):
522 while curr is not end:
526 # The second argument is an addition that causes a pylint warning.
527 def pop(self, last=True): # pylint: disable=W0221
529 raise KeyError('set is empty')
530 key = self.end[1][0] if last else self.end[2][0]
536 return '%s()' % (self.__class__.__name__,)
537 return '%s(%r)' % (self.__class__.__name__, list(self))
539 def __eq__(self, other):
540 if isinstance(other, OrderedSet):
541 return len(self) == len(other) and list(self) == list(other)
542 return set(self) == set(other)
544 # Extensions to the recipe.
545 def update(self, iterable):
551 class CycleError(Exception):
552 """An exception raised when an unexpected cycle is detected."""
553 def __init__(self, nodes):
556 return 'CycleError: cycle involving: ' + str(self.nodes)
559 def TopologicallySorted(graph, get_edges):
560 r"""Topologically sort based on a user provided edge definition.
563 graph: A list of node names.
564 get_edges: A function mapping from node name to a hashable collection
565 of node names which this node has outgoing edges to.
567 A list containing all of the node in graph in topological order.
568 It is assumed that calling get_edges once for each node and caching is
569 cheaper than repeatedly calling get_edges.
571 CycleError in the event of a cycle.
573 graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'}
575 return re.findall(r'\$\(([^))]\)', graph[node])
576 print TopologicallySorted(graph.keys(), GetEdges)
580 get_edges = memoize(get_edges)
586 raise CycleError(visiting)
591 for neighbor in get_edges(node):
593 visiting.remove(node)
594 ordered_nodes.insert(0, node)
595 for node in sorted(graph):
599 def CrossCompileRequested():
600 # TODO: figure out how to not build extra host objects in the
601 # non-cross-compile case when this is enabled, and enable unconditionally.
602 return (os.environ.get('GYP_CROSSCOMPILE') or
603 os.environ.get('AR_host') or
604 os.environ.get('CC_host') or
605 os.environ.get('CXX_host') or
606 os.environ.get('AR_target') or
607 os.environ.get('CC_target') or
608 os.environ.get('CXX_target'))