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