• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 errno
8import filecmp
9import os.path
10import re
11import tempfile
12import sys
13
14
15# A minimal memoizing decorator. It'll blow up if the args aren't immutable,
16# among other "problems".
17class memoize(object):
18  def __init__(self, func):
19    self.func = func
20    self.cache = {}
21  def __call__(self, *args):
22    try:
23      return self.cache[args]
24    except KeyError:
25      result = self.func(*args)
26      self.cache[args] = result
27      return result
28
29
30class GypError(Exception):
31  """Error class representing an error, which is to be presented
32  to the user.  The main entry point will catch and display this.
33  """
34  pass
35
36
37def ExceptionAppend(e, msg):
38  """Append a message to the given exception's message."""
39  if not e.args:
40    e.args = (msg,)
41  elif len(e.args) == 1:
42    e.args = (str(e.args[0]) + ' ' + msg,)
43  else:
44    e.args = (str(e.args[0]) + ' ' + msg,) + e.args[1:]
45
46
47def FindQualifiedTargets(target, qualified_list):
48  """
49  Given a list of qualified targets, return the qualified targets for the
50  specified |target|.
51  """
52  return [t for t in qualified_list if ParseQualifiedTarget(t)[1] == target]
53
54
55def ParseQualifiedTarget(target):
56  # Splits a qualified target into a build file, target name and toolset.
57
58  # NOTE: rsplit is used to disambiguate the Windows drive letter separator.
59  target_split = target.rsplit(':', 1)
60  if len(target_split) == 2:
61    [build_file, target] = target_split
62  else:
63    build_file = None
64
65  target_split = target.rsplit('#', 1)
66  if len(target_split) == 2:
67    [target, toolset] = target_split
68  else:
69    toolset = None
70
71  return [build_file, target, toolset]
72
73
74def ResolveTarget(build_file, target, toolset):
75  # This function resolves a target into a canonical form:
76  # - a fully defined build file, either absolute or relative to the current
77  # directory
78  # - a target name
79  # - a toolset
80  #
81  # build_file is the file relative to which 'target' is defined.
82  # target is the qualified target.
83  # toolset is the default toolset for that target.
84  [parsed_build_file, target, parsed_toolset] = ParseQualifiedTarget(target)
85
86  if parsed_build_file:
87    if build_file:
88      # If a relative path, parsed_build_file is relative to the directory
89      # containing build_file.  If build_file is not in the current directory,
90      # parsed_build_file is not a usable path as-is.  Resolve it by
91      # interpreting it as relative to build_file.  If parsed_build_file is
92      # absolute, it is usable as a path regardless of the current directory,
93      # and os.path.join will return it as-is.
94      build_file = os.path.normpath(os.path.join(os.path.dirname(build_file),
95                                                 parsed_build_file))
96      # Further (to handle cases like ../cwd), make it relative to cwd)
97      if not os.path.isabs(build_file):
98        build_file = RelativePath(build_file, '.')
99    else:
100      build_file = parsed_build_file
101
102  if parsed_toolset:
103    toolset = parsed_toolset
104
105  return [build_file, target, toolset]
106
107
108def BuildFile(fully_qualified_target):
109  # Extracts the build file from the fully qualified target.
110  return ParseQualifiedTarget(fully_qualified_target)[0]
111
112
113def GetEnvironFallback(var_list, default):
114  """Look up a key in the environment, with fallback to secondary keys
115  and finally falling back to a default value."""
116  for var in var_list:
117    if var in os.environ:
118      return os.environ[var]
119  return default
120
121
122def QualifiedTarget(build_file, target, toolset):
123  # "Qualified" means the file that a target was defined in and the target
124  # name, separated by a colon, suffixed by a # and the toolset name:
125  # /path/to/file.gyp:target_name#toolset
126  fully_qualified = build_file + ':' + target
127  if toolset:
128    fully_qualified = fully_qualified + '#' + toolset
129  return fully_qualified
130
131
132@memoize
133def RelativePath(path, relative_to):
134  # Assuming both |path| and |relative_to| are relative to the current
135  # directory, returns a relative path that identifies path relative to
136  # relative_to.
137
138  # Convert to normalized (and therefore absolute paths).
139  path = os.path.realpath(path)
140  relative_to = os.path.realpath(relative_to)
141
142  # On Windows, we can't create a relative path to a different drive, so just
143  # use the absolute path.
144  if sys.platform == 'win32':
145    if (os.path.splitdrive(path)[0].lower() !=
146        os.path.splitdrive(relative_to)[0].lower()):
147      return path
148
149  # Split the paths into components.
150  path_split = path.split(os.path.sep)
151  relative_to_split = relative_to.split(os.path.sep)
152
153  # Determine how much of the prefix the two paths share.
154  prefix_len = len(os.path.commonprefix([path_split, relative_to_split]))
155
156  # Put enough ".." components to back up out of relative_to to the common
157  # prefix, and then append the part of path_split after the common prefix.
158  relative_split = [os.path.pardir] * (len(relative_to_split) - prefix_len) + \
159                   path_split[prefix_len:]
160
161  if len(relative_split) == 0:
162    # The paths were the same.
163    return ''
164
165  # Turn it back into a string and we're done.
166  return os.path.join(*relative_split)
167
168
169@memoize
170def InvertRelativePath(path, toplevel_dir=None):
171  """Given a path like foo/bar that is relative to toplevel_dir, return
172  the inverse relative path back to the toplevel_dir.
173
174  E.g. os.path.normpath(os.path.join(path, InvertRelativePath(path)))
175  should always produce the empty string, unless the path contains symlinks.
176  """
177  if not path:
178    return path
179  toplevel_dir = '.' if toplevel_dir is None else toplevel_dir
180  return RelativePath(toplevel_dir, os.path.join(toplevel_dir, path))
181
182
183def FixIfRelativePath(path, relative_to):
184  # Like RelativePath but returns |path| unchanged if it is absolute.
185  if os.path.isabs(path):
186    return path
187  return RelativePath(path, relative_to)
188
189
190def UnrelativePath(path, relative_to):
191  # Assuming that |relative_to| is relative to the current directory, and |path|
192  # is a path relative to the dirname of |relative_to|, returns a path that
193  # identifies |path| relative to the current directory.
194  rel_dir = os.path.dirname(relative_to)
195  return os.path.normpath(os.path.join(rel_dir, path))
196
197
198# re objects used by EncodePOSIXShellArgument.  See IEEE 1003.1 XCU.2.2 at
199# http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02
200# and the documentation for various shells.
201
202# _quote is a pattern that should match any argument that needs to be quoted
203# with double-quotes by EncodePOSIXShellArgument.  It matches the following
204# characters appearing anywhere in an argument:
205#   \t, \n, space  parameter separators
206#   #              comments
207#   $              expansions (quoted to always expand within one argument)
208#   %              called out by IEEE 1003.1 XCU.2.2
209#   &              job control
210#   '              quoting
211#   (, )           subshell execution
212#   *, ?, [        pathname expansion
213#   ;              command delimiter
214#   <, >, |        redirection
215#   =              assignment
216#   {, }           brace expansion (bash)
217#   ~              tilde expansion
218# It also matches the empty string, because "" (or '') is the only way to
219# represent an empty string literal argument to a POSIX shell.
220#
221# This does not match the characters in _escape, because those need to be
222# backslash-escaped regardless of whether they appear in a double-quoted
223# string.
224_quote = re.compile('[\t\n #$%&\'()*;<=>?[{|}~]|^$')
225
226# _escape is a pattern that should match any character that needs to be
227# escaped with a backslash, whether or not the argument matched the _quote
228# pattern.  _escape is used with re.sub to backslash anything in _escape's
229# first match group, hence the (parentheses) in the regular expression.
230#
231# _escape matches the following characters appearing anywhere in an argument:
232#   "  to prevent POSIX shells from interpreting this character for quoting
233#   \  to prevent POSIX shells from interpreting this character for escaping
234#   `  to prevent POSIX shells from interpreting this character for command
235#      substitution
236# Missing from this list is $, because the desired behavior of
237# EncodePOSIXShellArgument is to permit parameter (variable) expansion.
238#
239# Also missing from this list is !, which bash will interpret as the history
240# expansion character when history is enabled.  bash does not enable history
241# by default in non-interactive shells, so this is not thought to be a problem.
242# ! was omitted from this list because bash interprets "\!" as a literal string
243# including the backslash character (avoiding history expansion but retaining
244# the backslash), which would not be correct for argument encoding.  Handling
245# this case properly would also be problematic because bash allows the history
246# character to be changed with the histchars shell variable.  Fortunately,
247# as history is not enabled in non-interactive shells and
248# EncodePOSIXShellArgument is only expected to encode for non-interactive
249# shells, there is no room for error here by ignoring !.
250_escape = re.compile(r'(["\\`])')
251
252def EncodePOSIXShellArgument(argument):
253  """Encodes |argument| suitably for consumption by POSIX shells.
254
255  argument may be quoted and escaped as necessary to ensure that POSIX shells
256  treat the returned value as a literal representing the argument passed to
257  this function.  Parameter (variable) expansions beginning with $ are allowed
258  to remain intact without escaping the $, to allow the argument to contain
259  references to variables to be expanded by the shell.
260  """
261
262  if not isinstance(argument, str):
263    argument = str(argument)
264
265  if _quote.search(argument):
266    quote = '"'
267  else:
268    quote = ''
269
270  encoded = quote + re.sub(_escape, r'\\\1', argument) + quote
271
272  return encoded
273
274
275def EncodePOSIXShellList(list):
276  """Encodes |list| suitably for consumption by POSIX shells.
277
278  Returns EncodePOSIXShellArgument for each item in list, and joins them
279  together using the space character as an argument separator.
280  """
281
282  encoded_arguments = []
283  for argument in list:
284    encoded_arguments.append(EncodePOSIXShellArgument(argument))
285  return ' '.join(encoded_arguments)
286
287
288def DeepDependencyTargets(target_dicts, roots):
289  """Returns the recursive list of target dependencies."""
290  dependencies = set()
291  pending = set(roots)
292  while pending:
293    # Pluck out one.
294    r = pending.pop()
295    # Skip if visited already.
296    if r in dependencies:
297      continue
298    # Add it.
299    dependencies.add(r)
300    # Add its children.
301    spec = target_dicts[r]
302    pending.update(set(spec.get('dependencies', [])))
303    pending.update(set(spec.get('dependencies_original', [])))
304  return list(dependencies - set(roots))
305
306
307def BuildFileTargets(target_list, build_file):
308  """From a target_list, returns the subset from the specified build_file.
309  """
310  return [p for p in target_list if BuildFile(p) == build_file]
311
312
313def AllTargets(target_list, target_dicts, build_file):
314  """Returns all targets (direct and dependencies) for the specified build_file.
315  """
316  bftargets = BuildFileTargets(target_list, build_file)
317  deptargets = DeepDependencyTargets(target_dicts, bftargets)
318  return bftargets + deptargets
319
320
321def WriteOnDiff(filename):
322  """Write to a file only if the new contents differ.
323
324  Arguments:
325    filename: name of the file to potentially write to.
326  Returns:
327    A file like object which will write to temporary file and only overwrite
328    the target if it differs (on close).
329  """
330
331  class Writer:
332    """Wrapper around file which only covers the target if it differs."""
333    def __init__(self):
334      # Pick temporary file.
335      tmp_fd, self.tmp_path = tempfile.mkstemp(
336          suffix='.tmp',
337          prefix=os.path.split(filename)[1] + '.gyp.',
338          dir=os.path.split(filename)[0])
339      try:
340        self.tmp_file = os.fdopen(tmp_fd, 'wb')
341      except Exception:
342        # Don't leave turds behind.
343        os.unlink(self.tmp_path)
344        raise
345
346    def __getattr__(self, attrname):
347      # Delegate everything else to self.tmp_file
348      return getattr(self.tmp_file, attrname)
349
350    def close(self):
351      try:
352        # Close tmp file.
353        self.tmp_file.close()
354        # Determine if different.
355        same = False
356        try:
357          same = filecmp.cmp(self.tmp_path, filename, False)
358        except OSError, e:
359          if e.errno != errno.ENOENT:
360            raise
361
362        if same:
363          # The new file is identical to the old one, just get rid of the new
364          # one.
365          os.unlink(self.tmp_path)
366        else:
367          # The new file is different from the old one, or there is no old one.
368          # Rename the new file to the permanent name.
369          #
370          # tempfile.mkstemp uses an overly restrictive mode, resulting in a
371          # file that can only be read by the owner, regardless of the umask.
372          # There's no reason to not respect the umask here, which means that
373          # an extra hoop is required to fetch it and reset the new file's mode.
374          #
375          # No way to get the umask without setting a new one?  Set a safe one
376          # and then set it back to the old value.
377          umask = os.umask(077)
378          os.umask(umask)
379          os.chmod(self.tmp_path, 0666 & ~umask)
380          if sys.platform == 'win32' and os.path.exists(filename):
381            # NOTE: on windows (but not cygwin) rename will not replace an
382            # existing file, so it must be preceded with a remove. Sadly there
383            # is no way to make the switch atomic.
384            os.remove(filename)
385          os.rename(self.tmp_path, filename)
386      except Exception:
387        # Don't leave turds behind.
388        os.unlink(self.tmp_path)
389        raise
390
391  return Writer()
392
393
394def EnsureDirExists(path):
395  """Make sure the directory for |path| exists."""
396  try:
397    os.makedirs(os.path.dirname(path))
398  except OSError:
399    pass
400
401
402def GetFlavor(params):
403  """Returns |params.flavor| if it's set, the system's default flavor else."""
404  flavors = {
405    'cygwin': 'win',
406    'win32': 'win',
407    'darwin': 'mac',
408  }
409
410  if 'flavor' in params:
411    return params['flavor']
412  if sys.platform in flavors:
413    return flavors[sys.platform]
414  if sys.platform.startswith('sunos'):
415    return 'solaris'
416  if sys.platform.startswith('freebsd'):
417    return 'freebsd'
418  if sys.platform.startswith('openbsd'):
419    return 'openbsd'
420  if sys.platform.startswith('aix'):
421    return 'aix'
422
423  return 'linux'
424
425
426def CopyTool(flavor, out_path):
427  """Finds (flock|mac|win)_tool.gyp in the gyp directory and copies it
428  to |out_path|."""
429  # aix and solaris just need flock emulation. mac and win use more complicated
430  # support scripts.
431  prefix = {
432      'aix': 'flock',
433      'solaris': 'flock',
434      'mac': 'mac',
435      'win': 'win'
436      }.get(flavor, None)
437  if not prefix:
438    return
439
440  # Slurp input file.
441  source_path = os.path.join(
442      os.path.dirname(os.path.abspath(__file__)), '%s_tool.py' % prefix)
443  with open(source_path) as source_file:
444    source = source_file.readlines()
445
446  # Add header and write it out.
447  tool_path = os.path.join(out_path, 'gyp-%s-tool' % prefix)
448  with open(tool_path, 'w') as tool_file:
449    tool_file.write(
450        ''.join([source[0], '# Generated by gyp. Do not edit.\n'] + source[1:]))
451
452  # Make file executable.
453  os.chmod(tool_path, 0755)
454
455
456# From Alex Martelli,
457# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560
458# ASPN: Python Cookbook: Remove duplicates from a sequence
459# First comment, dated 2001/10/13.
460# (Also in the printed Python Cookbook.)
461
462def uniquer(seq, idfun=None):
463    if idfun is None:
464        idfun = lambda x: x
465    seen = {}
466    result = []
467    for item in seq:
468        marker = idfun(item)
469        if marker in seen: continue
470        seen[marker] = 1
471        result.append(item)
472    return result
473
474
475class CycleError(Exception):
476  """An exception raised when an unexpected cycle is detected."""
477  def __init__(self, nodes):
478    self.nodes = nodes
479  def __str__(self):
480    return 'CycleError: cycle involving: ' + str(self.nodes)
481
482
483def TopologicallySorted(graph, get_edges):
484  """Topologically sort based on a user provided edge definition.
485
486  Args:
487    graph: A list of node names.
488    get_edges: A function mapping from node name to a hashable collection
489               of node names which this node has outgoing edges to.
490  Returns:
491    A list containing all of the node in graph in topological order.
492    It is assumed that calling get_edges once for each node and caching is
493    cheaper than repeatedly calling get_edges.
494  Raises:
495    CycleError in the event of a cycle.
496  Example:
497    graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'}
498    def GetEdges(node):
499      return re.findall(r'\$\(([^))]\)', graph[node])
500    print TopologicallySorted(graph.keys(), GetEdges)
501    ==>
502    ['a', 'c', b']
503  """
504  get_edges = memoize(get_edges)
505  visited = set()
506  visiting = set()
507  ordered_nodes = []
508  def Visit(node):
509    if node in visiting:
510      raise CycleError(visiting)
511    if node in visited:
512      return
513    visited.add(node)
514    visiting.add(node)
515    for neighbor in get_edges(node):
516      Visit(neighbor)
517    visiting.remove(node)
518    ordered_nodes.insert(0, node)
519  for node in sorted(graph):
520    Visit(node)
521  return ordered_nodes
522