1#!/usr/bin/python
2
3# Copyright (c) 2009 Google Inc. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import errno
8import filecmp
9import os.path
10import re
11import tempfile
12import sys
13
14def ExceptionAppend(e, msg):
15  """Append a message to the given exception's message."""
16  if not e.args:
17    e.args = (msg,)
18  elif len(e.args) == 1:
19    e.args = (str(e.args[0]) + ' ' + msg,)
20  else:
21    e.args = (str(e.args[0]) + ' ' + msg,) + e.args[1:]
22
23
24def ParseQualifiedTarget(target):
25  # Splits a qualified target into a build file, target name and toolset.
26
27  # NOTE: rsplit is used to disambiguate the Windows drive letter separator.
28  target_split = target.rsplit(':', 1)
29  if len(target_split) == 2:
30    [build_file, target] = target_split
31  else:
32    build_file = None
33
34  target_split = target.rsplit('#', 1)
35  if len(target_split) == 2:
36    [target, toolset] = target_split
37  else:
38    toolset = None
39
40  return [build_file, target, toolset]
41
42
43def ResolveTarget(build_file, target, toolset):
44  # This function resolves a target into a canonical form:
45  # - a fully defined build file, either absolute or relative to the current
46  # directory
47  # - a target name
48  # - a toolset
49  #
50  # build_file is the file relative to which 'target' is defined.
51  # target is the qualified target.
52  # toolset is the default toolset for that target.
53  [parsed_build_file, target, parsed_toolset] = ParseQualifiedTarget(target)
54
55  if parsed_build_file:
56    if build_file:
57      # If a relative path, parsed_build_file is relative to the directory
58      # containing build_file.  If build_file is not in the current directory,
59      # parsed_build_file is not a usable path as-is.  Resolve it by
60      # interpreting it as relative to build_file.  If parsed_build_file is
61      # absolute, it is usable as a path regardless of the current directory,
62      # and os.path.join will return it as-is.
63      build_file = os.path.normpath(os.path.join(os.path.dirname(build_file),
64                                                 parsed_build_file))
65    else:
66      build_file = parsed_build_file
67
68  if parsed_toolset:
69    toolset = parsed_toolset
70
71  return [build_file, target, toolset]
72
73
74def BuildFile(fully_qualified_target):
75  # Extracts the build file from the fully qualified target.
76  return ParseQualifiedTarget(fully_qualified_target)[0]
77
78
79def QualifiedTarget(build_file, target, toolset):
80  # "Qualified" means the file that a target was defined in and the target
81  # name, separated by a colon, suffixed by a # and the toolset name:
82  # /path/to/file.gyp:target_name#toolset
83  fully_qualified = build_file + ':' + target
84  if toolset:
85    fully_qualified = fully_qualified + '#' + toolset
86  return fully_qualified
87
88
89def RelativePath(path, relative_to):
90  # Assuming both |path| and |relative_to| are relative to the current
91  # directory, returns a relative path that identifies path relative to
92  # relative_to.
93
94  # Convert to absolute (and therefore normalized paths).
95  path = os.path.abspath(path)
96  relative_to = os.path.abspath(relative_to)
97
98  # Split the paths into components.
99  path_split = path.split(os.path.sep)
100  relative_to_split = relative_to.split(os.path.sep)
101
102  # Determine how much of the prefix the two paths share.
103  prefix_len = len(os.path.commonprefix([path_split, relative_to_split]))
104
105  # Put enough ".." components to back up out of relative_to to the common
106  # prefix, and then append the part of path_split after the common prefix.
107  relative_split = [os.path.pardir] * (len(relative_to_split) - prefix_len) + \
108                   path_split[prefix_len:]
109
110  if len(relative_split) == 0:
111    # The paths were the same.
112    return ''
113
114  # Turn it back into a string and we're done.
115  return os.path.join(*relative_split)
116
117
118def FixIfRelativePath(path, relative_to):
119  # Like RelativePath but returns |path| unchanged if it is absolute.
120  if os.path.isabs(path):
121    return path
122  return RelativePath(path, relative_to)
123
124
125def UnrelativePath(path, relative_to):
126  # Assuming that |relative_to| is relative to the current directory, and |path|
127  # is a path relative to the dirname of |relative_to|, returns a path that
128  # identifies |path| relative to the current directory.
129  rel_dir = os.path.dirname(relative_to)
130  return os.path.normpath(os.path.join(rel_dir, path))
131
132
133# re objects used by EncodePOSIXShellArgument.  See IEEE 1003.1 XCU.2.2 at
134# http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02
135# and the documentation for various shells.
136
137# _quote is a pattern that should match any argument that needs to be quoted
138# with double-quotes by EncodePOSIXShellArgument.  It matches the following
139# characters appearing anywhere in an argument:
140#   \t, \n, space  parameter separators
141#   #              comments
142#   $              expansions (quoted to always expand within one argument)
143#   %              called out by IEEE 1003.1 XCU.2.2
144#   &              job control
145#   '              quoting
146#   (, )           subshell execution
147#   *, ?, [        pathname expansion
148#   ;              command delimiter
149#   <, >, |        redirection
150#   =              assignment
151#   {, }           brace expansion (bash)
152#   ~              tilde expansion
153# It also matches the empty string, because "" (or '') is the only way to
154# represent an empty string literal argument to a POSIX shell.
155#
156# This does not match the characters in _escape, because those need to be
157# backslash-escaped regardless of whether they appear in a double-quoted
158# string.
159_quote = re.compile('[\t\n #$%&\'()*;<=>?[{|}~]|^$')
160
161# _escape is a pattern that should match any character that needs to be
162# escaped with a backslash, whether or not the argument matched the _quote
163# pattern.  _escape is used with re.sub to backslash anything in _escape's
164# first match group, hence the (parentheses) in the regular expression.
165#
166# _escape matches the following characters appearing anywhere in an argument:
167#   "  to prevent POSIX shells from interpreting this character for quoting
168#   \  to prevent POSIX shells from interpreting this character for escaping
169#   `  to prevent POSIX shells from interpreting this character for command
170#      substitution
171# Missing from this list is $, because the desired behavior of
172# EncodePOSIXShellArgument is to permit parameter (variable) expansion.
173#
174# Also missing from this list is !, which bash will interpret as the history
175# expansion character when history is enabled.  bash does not enable history
176# by default in non-interactive shells, so this is not thought to be a problem.
177# ! was omitted from this list because bash interprets "\!" as a literal string
178# including the backslash character (avoiding history expansion but retaining
179# the backslash), which would not be correct for argument encoding.  Handling
180# this case properly would also be problematic because bash allows the history
181# character to be changed with the histchars shell variable.  Fortunately,
182# as history is not enabled in non-interactive shells and
183# EncodePOSIXShellArgument is only expected to encode for non-interactive
184# shells, there is no room for error here by ignoring !.
185_escape = re.compile(r'(["\\`])')
186
187def EncodePOSIXShellArgument(argument):
188  """Encodes |argument| suitably for consumption by POSIX shells.
189
190  argument may be quoted and escaped as necessary to ensure that POSIX shells
191  treat the returned value as a literal representing the argument passed to
192  this function.  Parameter (variable) expansions beginning with $ are allowed
193  to remain intact without escaping the $, to allow the argument to contain
194  references to variables to be expanded by the shell.
195  """
196
197  if not isinstance(argument, str):
198    argument = str(argument)
199
200  if _quote.search(argument):
201    quote = '"'
202  else:
203    quote = ''
204
205  encoded = quote + re.sub(_escape, r'\\\1', argument) + quote
206
207  return encoded
208
209
210def EncodePOSIXShellList(list):
211  """Encodes |list| suitably for consumption by POSIX shells.
212
213  Returns EncodePOSIXShellArgument for each item in list, and joins them
214  together using the space character as an argument separator.
215  """
216
217  encoded_arguments = []
218  for argument in list:
219    encoded_arguments.append(EncodePOSIXShellArgument(argument))
220  return ' '.join(encoded_arguments)
221
222
223def DeepDependencyTargets(target_dicts, roots):
224  """Returns the recursive list of target dependencies."""
225  dependencies = set()
226  pending = set(roots)
227  while pending:
228    # Pluck out one.
229    r = pending.pop()
230    # Skip if visited already.
231    if r in dependencies:
232      continue
233    # Add it.
234    dependencies.add(r)
235    # Add its children.
236    spec = target_dicts[r]
237    pending.update(set(spec.get('dependencies', [])))
238    pending.update(set(spec.get('dependencies_original', [])))
239  return list(dependencies - set(roots))
240
241
242def BuildFileTargets(target_list, build_file):
243  """From a target_list, returns the subset from the specified build_file.
244  """
245  return [p for p in target_list if BuildFile(p) == build_file]
246
247
248def AllTargets(target_list, target_dicts, build_file):
249  """Returns all targets (direct and dependencies) for the specified build_file.
250  """
251  bftargets = BuildFileTargets(target_list, build_file)
252  deptargets = DeepDependencyTargets(target_dicts, bftargets)
253  return bftargets + deptargets
254
255
256def WriteOnDiff(filename):
257  """Write to a file only if the new contents differ.
258
259  Arguments:
260    filename: name of the file to potentially write to.
261  Returns:
262    A file like object which will write to temporary file and only overwrite
263    the target if it differs (on close).
264  """
265
266  class Writer:
267    """Wrapper around file which only covers the target if it differs."""
268    def __init__(self):
269      # Pick temporary file.
270      tmp_fd, self.tmp_path = tempfile.mkstemp(
271          suffix='.tmp',
272          prefix=os.path.split(filename)[1] + '.gyp.',
273          dir=os.path.split(filename)[0])
274      try:
275        self.tmp_file = os.fdopen(tmp_fd, 'wb')
276      except Exception:
277        # Don't leave turds behind.
278        os.unlink(self.tmp_path)
279        raise
280
281    def __getattr__(self, attrname):
282      # Delegate everything else to self.tmp_file
283      return getattr(self.tmp_file, attrname)
284
285    def close(self):
286      try:
287        # Close tmp file.
288        self.tmp_file.close()
289        # Determine if different.
290        same = False
291        try:
292          same = filecmp.cmp(self.tmp_path, filename, False)
293        except OSError, e:
294          if e.errno != errno.ENOENT:
295            raise
296
297        if same:
298          # The new file is identical to the old one, just get rid of the new
299          # one.
300          os.unlink(self.tmp_path)
301        else:
302          # The new file is different from the old one, or there is no old one.
303          # Rename the new file to the permanent name.
304          #
305          # tempfile.mkstemp uses an overly restrictive mode, resulting in a
306          # file that can only be read by the owner, regardless of the umask.
307          # There's no reason to not respect the umask here, which means that
308          # an extra hoop is required to fetch it and reset the new file's mode.
309          #
310          # No way to get the umask without setting a new one?  Set a safe one
311          # and then set it back to the old value.
312          umask = os.umask(077)
313          os.umask(umask)
314          os.chmod(self.tmp_path, 0666 & ~umask)
315          if sys.platform == 'win32' and os.path.exists(filename):
316            # NOTE: on windows (but not cygwin) rename will not replace an
317            # existing file, so it must be preceded with a remove. Sadly there
318            # is no way to make the switch atomic.
319            os.remove(filename)
320          os.rename(self.tmp_path, filename)
321      except Exception:
322        # Don't leave turds behind.
323        os.unlink(self.tmp_path)
324        raise
325
326  return Writer()
327
328
329# From Alex Martelli,
330# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560
331# ASPN: Python Cookbook: Remove duplicates from a sequence
332# First comment, dated 2001/10/13.
333# (Also in the printed Python Cookbook.)
334
335def uniquer(seq, idfun=None):
336    if idfun is None:
337        def idfun(x): return x
338    seen = {}
339    result = []
340    for item in seq:
341        marker = idfun(item)
342        if marker in seen: continue
343        seen[marker] = 1
344        result.append(item)
345    return result
346