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.
4
5from __future__ import with_statement
6
7import collections
8import errno
9import filecmp
10import os.path
11import re
12import tempfile
13import sys
14
15
16# A minimal memoizing decorator. It'll blow up if the args aren't immutable,
17# among other "problems".
18class memoize(object):
19  def __init__(self, func):
20    self.func = func
21    self.cache = {}
22  def __call__(self, *args):
23    try:
24      return self.cache[args]
25    except KeyError:
26      result = self.func(*args)
27      self.cache[args] = result
28      return result
29
30
31class 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.
34  """
35  pass
36
37
38def ExceptionAppend(e, msg):
39  """Append a message to the given exception's message."""
40  if not e.args:
41    e.args = (msg,)
42  elif len(e.args) == 1:
43    e.args = (str(e.args[0]) + ' ' + msg,)
44  else:
45    e.args = (str(e.args[0]) + ' ' + msg,) + e.args[1:]
46
47
48def FindQualifiedTargets(target, qualified_list):
49  """
50  Given a list of qualified targets, return the qualified targets for the
51  specified |target|.
52  """
53  return [t for t in qualified_list if ParseQualifiedTarget(t)[1] == target]
54
55
56def ParseQualifiedTarget(target):
57  # Splits a qualified target into a build file, target name and toolset.
58
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
63  else:
64    build_file = None
65
66  target_split = target.rsplit('#', 1)
67  if len(target_split) == 2:
68    [target, toolset] = target_split
69  else:
70    toolset = None
71
72  return [build_file, target, toolset]
73
74
75def 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
78  # directory
79  # - a target name
80  # - a toolset
81  #
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)
86
87  if parsed_build_file:
88    if build_file:
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),
96                                                 parsed_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, '.')
100    else:
101      build_file = parsed_build_file
102
103  if parsed_toolset:
104    toolset = parsed_toolset
105
106  return [build_file, target, toolset]
107
108
109def BuildFile(fully_qualified_target):
110  # Extracts the build file from the fully qualified target.
111  return ParseQualifiedTarget(fully_qualified_target)[0]
112
113
114def 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."""
117  for var in var_list:
118    if var in os.environ:
119      return os.environ[var]
120  return default
121
122
123def 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
128  if toolset:
129    fully_qualified = fully_qualified + '#' + toolset
130  return fully_qualified
131
132
133@memoize
134def 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
137  # 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.
142
143  # Convert to normalized (and therefore absolute paths).
144  if follow_path_symlink:
145    path = os.path.realpath(path)
146  else:
147    path = os.path.abspath(path)
148  relative_to = os.path.realpath(relative_to)
149
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()):
155      return path
156
157  # Split the paths into components.
158  path_split = path.split(os.path.sep)
159  relative_to_split = relative_to.split(os.path.sep)
160
161  # Determine how much of the prefix the two paths share.
162  prefix_len = len(os.path.commonprefix([path_split, relative_to_split]))
163
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:]
168
169  if len(relative_split) == 0:
170    # The paths were the same.
171    return ''
172
173  # Turn it back into a string and we're done.
174  return os.path.join(*relative_split)
175
176
177@memoize
178def 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.
181
182  E.g. os.path.normpath(os.path.join(path, InvertRelativePath(path)))
183  should always produce the empty string, unless the path contains symlinks.
184  """
185  if not path:
186    return path
187  toplevel_dir = '.' if toplevel_dir is None else toplevel_dir
188  return RelativePath(toplevel_dir, os.path.join(toplevel_dir, path))
189
190
191def FixIfRelativePath(path, relative_to):
192  # Like RelativePath but returns |path| unchanged if it is absolute.
193  if os.path.isabs(path):
194    return path
195  return RelativePath(path, relative_to)
196
197
198def 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))
204
205
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.
209
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
214#   #              comments
215#   $              expansions (quoted to always expand within one argument)
216#   %              called out by IEEE 1003.1 XCU.2.2
217#   &              job control
218#   '              quoting
219#   (, )           subshell execution
220#   *, ?, [        pathname expansion
221#   ;              command delimiter
222#   <, >, |        redirection
223#   =              assignment
224#   {, }           brace expansion (bash)
225#   ~              tilde expansion
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.
228#
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
231# string.
232_quote = re.compile('[\t\n #$%&\'()*;<=>?[{|}~]|^$')
233
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.
238#
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
243#      substitution
244# Missing from this list is $, because the desired behavior of
245# EncodePOSIXShellArgument is to permit parameter (variable) expansion.
246#
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'(["\\`])')
259
260def EncodePOSIXShellArgument(argument):
261  """Encodes |argument| suitably for consumption by POSIX shells.
262
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.
268  """
269
270  if not isinstance(argument, str):
271    argument = str(argument)
272
273  if _quote.search(argument):
274    quote = '"'
275  else:
276    quote = ''
277
278  encoded = quote + re.sub(_escape, r'\\\1', argument) + quote
279
280  return encoded
281
282
283def EncodePOSIXShellList(list):
284  """Encodes |list| suitably for consumption by POSIX shells.
285
286  Returns EncodePOSIXShellArgument for each item in list, and joins them
287  together using the space character as an argument separator.
288  """
289
290  encoded_arguments = []
291  for argument in list:
292    encoded_arguments.append(EncodePOSIXShellArgument(argument))
293  return ' '.join(encoded_arguments)
294
295
296def DeepDependencyTargets(target_dicts, roots):
297  """Returns the recursive list of target dependencies."""
298  dependencies = set()
299  pending = set(roots)
300  while pending:
301    # Pluck out one.
302    r = pending.pop()
303    # Skip if visited already.
304    if r in dependencies:
305      continue
306    # Add it.
307    dependencies.add(r)
308    # Add its children.
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))
313
314
315def BuildFileTargets(target_list, build_file):
316  """From a target_list, returns the subset from the specified build_file.
317  """
318  return [p for p in target_list if BuildFile(p) == build_file]
319
320
321def AllTargets(target_list, target_dicts, build_file):
322  """Returns all targets (direct and dependencies) for the specified build_file.
323  """
324  bftargets = BuildFileTargets(target_list, build_file)
325  deptargets = DeepDependencyTargets(target_dicts, bftargets)
326  return bftargets + deptargets
327
328
329def WriteOnDiff(filename):
330  """Write to a file only if the new contents differ.
331
332  Arguments:
333    filename: name of the file to potentially write to.
334  Returns:
335    A file like object which will write to temporary file and only overwrite
336    the target if it differs (on close).
337  """
338
339  class Writer(object):
340    """Wrapper around file which only covers the target if it differs."""
341    def __init__(self):
342      # Pick temporary file.
343      tmp_fd, self.tmp_path = tempfile.mkstemp(
344          suffix='.tmp',
345          prefix=os.path.split(filename)[1] + '.gyp.',
346          dir=os.path.split(filename)[0])
347      try:
348        self.tmp_file = os.fdopen(tmp_fd, 'wb')
349      except Exception:
350        # Don't leave turds behind.
351        os.unlink(self.tmp_path)
352        raise
353
354    def __getattr__(self, attrname):
355      # Delegate everything else to self.tmp_file
356      return getattr(self.tmp_file, attrname)
357
358    def close(self):
359      try:
360        # Close tmp file.
361        self.tmp_file.close()
362        # Determine if different.
363        same = False
364        try:
365          same = filecmp.cmp(self.tmp_path, filename, False)
366        except OSError, e:
367          if e.errno != errno.ENOENT:
368            raise
369
370        if same:
371          # The new file is identical to the old one, just get rid of the new
372          # one.
373          os.unlink(self.tmp_path)
374        else:
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.
377          #
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.
382          #
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)
386          os.umask(umask)
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.
392            os.remove(filename)
393          os.rename(self.tmp_path, filename)
394      except Exception:
395        # Don't leave turds behind.
396        os.unlink(self.tmp_path)
397        raise
398
399  return Writer()
400
401
402def EnsureDirExists(path):
403  """Make sure the directory for |path| exists."""
404  try:
405    os.makedirs(os.path.dirname(path))
406  except OSError:
407    pass
408
409
410def GetFlavor(params):
411  """Returns |params.flavor| if it's set, the system's default flavor else."""
412  flavors = {
413    'cygwin': 'win',
414    'win32': 'win',
415    'darwin': 'mac',
416  }
417
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'):
423    return 'solaris'
424  if sys.platform.startswith('freebsd') or sys.platform.startswith('dragonfly'):
425    return 'freebsd'
426  if sys.platform.startswith('openbsd'):
427    return 'openbsd'
428  if sys.platform.startswith('netbsd'):
429    return 'netbsd'
430  if sys.platform.startswith('aix'):
431    return 'aix'
432
433  return 'linux'
434
435
436def CopyTool(flavor, out_path, generator_flags={}):
437  """Finds (flock|mac|win)_tool.gyp in the gyp directory and copies it
438  to |out_path|."""
439  # aix and solaris just need flock emulation. mac and win use more complicated
440  # support scripts.
441  prefix = {
442      'aix': 'flock',
443      'solaris': 'flock',
444      'mac': 'mac',
445      'win': 'win'
446      }.get(flavor, None)
447  if not prefix:
448    return
449
450  # Slurp input file.
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()
455
456  # Set custom header flags.
457  header = '# Generated by gyp. Do not edit.\n'
458  mac_toolchain_dir =  generator_flags.get('mac_toolchain_dir', None)
459  if flavor == 'mac' and mac_toolchain_dir:
460    header += "import os;\nos.environ['DEVELOPER_DIR']='%s'\n" \
461        % mac_toolchain_dir
462
463  # Add header and write it out.
464  tool_path = os.path.join(out_path, 'gyp-%s-tool' % prefix)
465  with open(tool_path, 'w') as tool_file:
466    tool_file.write(
467        ''.join([source[0], header] + source[1:]))
468
469  # Make file executable.
470  os.chmod(tool_path, 0755)
471
472
473# From Alex Martelli,
474# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560
475# ASPN: Python Cookbook: Remove duplicates from a sequence
476# First comment, dated 2001/10/13.
477# (Also in the printed Python Cookbook.)
478
479def uniquer(seq, idfun=None):
480    if idfun is None:
481        idfun = lambda x: x
482    seen = {}
483    result = []
484    for item in seq:
485        marker = idfun(item)
486        if marker in seen: continue
487        seen[marker] = 1
488        result.append(item)
489    return result
490
491
492# Based on http://code.activestate.com/recipes/576694/.
493class OrderedSet(collections.MutableSet):
494  def __init__(self, iterable=None):
495    self.end = end = []
496    end += [None, end, end]         # sentinel node for doubly linked list
497    self.map = {}                   # key --> [key, prev, next]
498    if iterable is not None:
499      self |= iterable
500
501  def __len__(self):
502    return len(self.map)
503
504  def __contains__(self, key):
505    return key in self.map
506
507  def add(self, key):
508    if key not in self.map:
509      end = self.end
510      curr = end[1]
511      curr[2] = end[1] = self.map[key] = [key, curr, end]
512
513  def discard(self, key):
514    if key in self.map:
515      key, prev_item, next_item = self.map.pop(key)
516      prev_item[2] = next_item
517      next_item[1] = prev_item
518
519  def __iter__(self):
520    end = self.end
521    curr = end[2]
522    while curr is not end:
523      yield curr[0]
524      curr = curr[2]
525
526  def __reversed__(self):
527    end = self.end
528    curr = end[1]
529    while curr is not end:
530      yield curr[0]
531      curr = curr[1]
532
533  # The second argument is an addition that causes a pylint warning.
534  def pop(self, last=True):  # pylint: disable=W0221
535    if not self:
536      raise KeyError('set is empty')
537    key = self.end[1][0] if last else self.end[2][0]
538    self.discard(key)
539    return key
540
541  def __repr__(self):
542    if not self:
543      return '%s()' % (self.__class__.__name__,)
544    return '%s(%r)' % (self.__class__.__name__, list(self))
545
546  def __eq__(self, other):
547    if isinstance(other, OrderedSet):
548      return len(self) == len(other) and list(self) == list(other)
549    return set(self) == set(other)
550
551  # Extensions to the recipe.
552  def update(self, iterable):
553    for i in iterable:
554      if i not in self:
555        self.add(i)
556
557
558class CycleError(Exception):
559  """An exception raised when an unexpected cycle is detected."""
560  def __init__(self, nodes):
561    self.nodes = nodes
562  def __str__(self):
563    return 'CycleError: cycle involving: ' + str(self.nodes)
564
565
566def TopologicallySorted(graph, get_edges):
567  r"""Topologically sort based on a user provided edge definition.
568
569  Args:
570    graph: A list of node names.
571    get_edges: A function mapping from node name to a hashable collection
572               of node names which this node has outgoing edges to.
573  Returns:
574    A list containing all of the node in graph in topological order.
575    It is assumed that calling get_edges once for each node and caching is
576    cheaper than repeatedly calling get_edges.
577  Raises:
578    CycleError in the event of a cycle.
579  Example:
580    graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'}
581    def GetEdges(node):
582      return re.findall(r'\$\(([^))]\)', graph[node])
583    print TopologicallySorted(graph.keys(), GetEdges)
584    ==>
585    ['a', 'c', b']
586  """
587  get_edges = memoize(get_edges)
588  visited = set()
589  visiting = set()
590  ordered_nodes = []
591  def Visit(node):
592    if node in visiting:
593      raise CycleError(visiting)
594    if node in visited:
595      return
596    visited.add(node)
597    visiting.add(node)
598    for neighbor in get_edges(node):
599      Visit(neighbor)
600    visiting.remove(node)
601    ordered_nodes.insert(0, node)
602  for node in sorted(graph):
603    Visit(node)
604  return ordered_nodes
605
606def CrossCompileRequested():
607  # TODO: figure out how to not build extra host objects in the
608  # non-cross-compile case when this is enabled, and enable unconditionally.
609  return (os.environ.get('GYP_CROSSCOMPILE') or
610          os.environ.get('AR_host') or
611          os.environ.get('CC_host') or
612          os.environ.get('CXX_host') or
613          os.environ.get('AR_target') or
614          os.environ.get('CC_target') or
615          os.environ.get('CXX_target'))
616