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): 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 139 # Convert to normalized (and therefore absolute paths). 140 path = os.path.realpath(path) 141 relative_to = os.path.realpath(relative_to) 142 143 # On Windows, we can't create a relative path to a different drive, so just 144 # use the absolute path. 145 if sys.platform == 'win32': 146 if (os.path.splitdrive(path)[0].lower() != 147 os.path.splitdrive(relative_to)[0].lower()): 148 return path 149 150 # Split the paths into components. 151 path_split = path.split(os.path.sep) 152 relative_to_split = relative_to.split(os.path.sep) 153 154 # Determine how much of the prefix the two paths share. 155 prefix_len = len(os.path.commonprefix([path_split, relative_to_split])) 156 157 # Put enough ".." components to back up out of relative_to to the common 158 # prefix, and then append the part of path_split after the common prefix. 159 relative_split = [os.path.pardir] * (len(relative_to_split) - prefix_len) + \ 160 path_split[prefix_len:] 161 162 if len(relative_split) == 0: 163 # The paths were the same. 164 return '' 165 166 # Turn it back into a string and we're done. 167 return os.path.join(*relative_split) 168 169 170@memoize 171def InvertRelativePath(path, toplevel_dir=None): 172 """Given a path like foo/bar that is relative to toplevel_dir, return 173 the inverse relative path back to the toplevel_dir. 174 175 E.g. os.path.normpath(os.path.join(path, InvertRelativePath(path))) 176 should always produce the empty string, unless the path contains symlinks. 177 """ 178 if not path: 179 return path 180 toplevel_dir = '.' if toplevel_dir is None else toplevel_dir 181 return RelativePath(toplevel_dir, os.path.join(toplevel_dir, path)) 182 183 184def FixIfRelativePath(path, relative_to): 185 # Like RelativePath but returns |path| unchanged if it is absolute. 186 if os.path.isabs(path): 187 return path 188 return RelativePath(path, relative_to) 189 190 191def UnrelativePath(path, relative_to): 192 # Assuming that |relative_to| is relative to the current directory, and |path| 193 # is a path relative to the dirname of |relative_to|, returns a path that 194 # identifies |path| relative to the current directory. 195 rel_dir = os.path.dirname(relative_to) 196 return os.path.normpath(os.path.join(rel_dir, path)) 197 198 199# re objects used by EncodePOSIXShellArgument. See IEEE 1003.1 XCU.2.2 at 200# http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02 201# and the documentation for various shells. 202 203# _quote is a pattern that should match any argument that needs to be quoted 204# with double-quotes by EncodePOSIXShellArgument. It matches the following 205# characters appearing anywhere in an argument: 206# \t, \n, space parameter separators 207# # comments 208# $ expansions (quoted to always expand within one argument) 209# % called out by IEEE 1003.1 XCU.2.2 210# & job control 211# ' quoting 212# (, ) subshell execution 213# *, ?, [ pathname expansion 214# ; command delimiter 215# <, >, | redirection 216# = assignment 217# {, } brace expansion (bash) 218# ~ tilde expansion 219# It also matches the empty string, because "" (or '') is the only way to 220# represent an empty string literal argument to a POSIX shell. 221# 222# This does not match the characters in _escape, because those need to be 223# backslash-escaped regardless of whether they appear in a double-quoted 224# string. 225_quote = re.compile('[\t\n #$%&\'()*;<=>?[{|}~]|^$') 226 227# _escape is a pattern that should match any character that needs to be 228# escaped with a backslash, whether or not the argument matched the _quote 229# pattern. _escape is used with re.sub to backslash anything in _escape's 230# first match group, hence the (parentheses) in the regular expression. 231# 232# _escape matches the following characters appearing anywhere in an argument: 233# " to prevent POSIX shells from interpreting this character for quoting 234# \ to prevent POSIX shells from interpreting this character for escaping 235# ` to prevent POSIX shells from interpreting this character for command 236# substitution 237# Missing from this list is $, because the desired behavior of 238# EncodePOSIXShellArgument is to permit parameter (variable) expansion. 239# 240# Also missing from this list is !, which bash will interpret as the history 241# expansion character when history is enabled. bash does not enable history 242# by default in non-interactive shells, so this is not thought to be a problem. 243# ! was omitted from this list because bash interprets "\!" as a literal string 244# including the backslash character (avoiding history expansion but retaining 245# the backslash), which would not be correct for argument encoding. Handling 246# this case properly would also be problematic because bash allows the history 247# character to be changed with the histchars shell variable. Fortunately, 248# as history is not enabled in non-interactive shells and 249# EncodePOSIXShellArgument is only expected to encode for non-interactive 250# shells, there is no room for error here by ignoring !. 251_escape = re.compile(r'(["\\`])') 252 253def EncodePOSIXShellArgument(argument): 254 """Encodes |argument| suitably for consumption by POSIX shells. 255 256 argument may be quoted and escaped as necessary to ensure that POSIX shells 257 treat the returned value as a literal representing the argument passed to 258 this function. Parameter (variable) expansions beginning with $ are allowed 259 to remain intact without escaping the $, to allow the argument to contain 260 references to variables to be expanded by the shell. 261 """ 262 263 if not isinstance(argument, str): 264 argument = str(argument) 265 266 if _quote.search(argument): 267 quote = '"' 268 else: 269 quote = '' 270 271 encoded = quote + re.sub(_escape, r'\\\1', argument) + quote 272 273 return encoded 274 275 276def EncodePOSIXShellList(list): 277 """Encodes |list| suitably for consumption by POSIX shells. 278 279 Returns EncodePOSIXShellArgument for each item in list, and joins them 280 together using the space character as an argument separator. 281 """ 282 283 encoded_arguments = [] 284 for argument in list: 285 encoded_arguments.append(EncodePOSIXShellArgument(argument)) 286 return ' '.join(encoded_arguments) 287 288 289def DeepDependencyTargets(target_dicts, roots): 290 """Returns the recursive list of target dependencies.""" 291 dependencies = set() 292 pending = set(roots) 293 while pending: 294 # Pluck out one. 295 r = pending.pop() 296 # Skip if visited already. 297 if r in dependencies: 298 continue 299 # Add it. 300 dependencies.add(r) 301 # Add its children. 302 spec = target_dicts[r] 303 pending.update(set(spec.get('dependencies', []))) 304 pending.update(set(spec.get('dependencies_original', []))) 305 return list(dependencies - set(roots)) 306 307 308def BuildFileTargets(target_list, build_file): 309 """From a target_list, returns the subset from the specified build_file. 310 """ 311 return [p for p in target_list if BuildFile(p) == build_file] 312 313 314def AllTargets(target_list, target_dicts, build_file): 315 """Returns all targets (direct and dependencies) for the specified build_file. 316 """ 317 bftargets = BuildFileTargets(target_list, build_file) 318 deptargets = DeepDependencyTargets(target_dicts, bftargets) 319 return bftargets + deptargets 320 321 322def WriteOnDiff(filename): 323 """Write to a file only if the new contents differ. 324 325 Arguments: 326 filename: name of the file to potentially write to. 327 Returns: 328 A file like object which will write to temporary file and only overwrite 329 the target if it differs (on close). 330 """ 331 332 class Writer: 333 """Wrapper around file which only covers the target if it differs.""" 334 def __init__(self): 335 # Pick temporary file. 336 tmp_fd, self.tmp_path = tempfile.mkstemp( 337 suffix='.tmp', 338 prefix=os.path.split(filename)[1] + '.gyp.', 339 dir=os.path.split(filename)[0]) 340 try: 341 self.tmp_file = os.fdopen(tmp_fd, 'wb') 342 except Exception: 343 # Don't leave turds behind. 344 os.unlink(self.tmp_path) 345 raise 346 347 def __getattr__(self, attrname): 348 # Delegate everything else to self.tmp_file 349 return getattr(self.tmp_file, attrname) 350 351 def close(self): 352 try: 353 # Close tmp file. 354 self.tmp_file.close() 355 # Determine if different. 356 same = False 357 try: 358 same = filecmp.cmp(self.tmp_path, filename, False) 359 except OSError, e: 360 if e.errno != errno.ENOENT: 361 raise 362 363 if same: 364 # The new file is identical to the old one, just get rid of the new 365 # one. 366 os.unlink(self.tmp_path) 367 else: 368 # The new file is different from the old one, or there is no old one. 369 # Rename the new file to the permanent name. 370 # 371 # tempfile.mkstemp uses an overly restrictive mode, resulting in a 372 # file that can only be read by the owner, regardless of the umask. 373 # There's no reason to not respect the umask here, which means that 374 # an extra hoop is required to fetch it and reset the new file's mode. 375 # 376 # No way to get the umask without setting a new one? Set a safe one 377 # and then set it back to the old value. 378 umask = os.umask(077) 379 os.umask(umask) 380 os.chmod(self.tmp_path, 0666 & ~umask) 381 if sys.platform == 'win32' and os.path.exists(filename): 382 # NOTE: on windows (but not cygwin) rename will not replace an 383 # existing file, so it must be preceded with a remove. Sadly there 384 # is no way to make the switch atomic. 385 os.remove(filename) 386 os.rename(self.tmp_path, filename) 387 except Exception: 388 # Don't leave turds behind. 389 os.unlink(self.tmp_path) 390 raise 391 392 return Writer() 393 394 395def EnsureDirExists(path): 396 """Make sure the directory for |path| exists.""" 397 try: 398 os.makedirs(os.path.dirname(path)) 399 except OSError: 400 pass 401 402 403def GetFlavor(params): 404 """Returns |params.flavor| if it's set, the system's default flavor else.""" 405 flavors = { 406 'cygwin': 'win', 407 'win32': 'win', 408 'darwin': 'mac', 409 } 410 411 if 'flavor' in params: 412 return params['flavor'] 413 if sys.platform in flavors: 414 return flavors[sys.platform] 415 if sys.platform.startswith('sunos'): 416 return 'solaris' 417 if sys.platform.startswith('freebsd'): 418 return 'freebsd' 419 if sys.platform.startswith('openbsd'): 420 return 'openbsd' 421 if sys.platform.startswith('aix'): 422 return 'aix' 423 424 return 'linux' 425 426 427def CopyTool(flavor, out_path): 428 """Finds (flock|mac|win)_tool.gyp in the gyp directory and copies it 429 to |out_path|.""" 430 # aix and solaris just need flock emulation. mac and win use more complicated 431 # support scripts. 432 prefix = { 433 'aix': 'flock', 434 'solaris': 'flock', 435 'mac': 'mac', 436 'win': 'win' 437 }.get(flavor, None) 438 if not prefix: 439 return 440 441 # Slurp input file. 442 source_path = os.path.join( 443 os.path.dirname(os.path.abspath(__file__)), '%s_tool.py' % prefix) 444 with open(source_path) as source_file: 445 source = source_file.readlines() 446 447 # Add header and write it out. 448 tool_path = os.path.join(out_path, 'gyp-%s-tool' % prefix) 449 with open(tool_path, 'w') as tool_file: 450 tool_file.write( 451 ''.join([source[0], '# Generated by gyp. Do not edit.\n'] + source[1:])) 452 453 # Make file executable. 454 os.chmod(tool_path, 0755) 455 456 457# From Alex Martelli, 458# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560 459# ASPN: Python Cookbook: Remove duplicates from a sequence 460# First comment, dated 2001/10/13. 461# (Also in the printed Python Cookbook.) 462 463def uniquer(seq, idfun=None): 464 if idfun is None: 465 idfun = lambda x: x 466 seen = {} 467 result = [] 468 for item in seq: 469 marker = idfun(item) 470 if marker in seen: continue 471 seen[marker] = 1 472 result.append(item) 473 return result 474 475 476# Based on http://code.activestate.com/recipes/576694/. 477class OrderedSet(collections.MutableSet): 478 def __init__(self, iterable=None): 479 self.end = end = [] 480 end += [None, end, end] # sentinel node for doubly linked list 481 self.map = {} # key --> [key, prev, next] 482 if iterable is not None: 483 self |= iterable 484 485 def __len__(self): 486 return len(self.map) 487 488 def __contains__(self, key): 489 return key in self.map 490 491 def add(self, key): 492 if key not in self.map: 493 end = self.end 494 curr = end[1] 495 curr[2] = end[1] = self.map[key] = [key, curr, end] 496 497 def discard(self, key): 498 if key in self.map: 499 key, prev_item, next_item = self.map.pop(key) 500 prev_item[2] = next_item 501 next_item[1] = prev_item 502 503 def __iter__(self): 504 end = self.end 505 curr = end[2] 506 while curr is not end: 507 yield curr[0] 508 curr = curr[2] 509 510 def __reversed__(self): 511 end = self.end 512 curr = end[1] 513 while curr is not end: 514 yield curr[0] 515 curr = curr[1] 516 517 # The second argument is an addition that causes a pylint warning. 518 def pop(self, last=True): # pylint: disable=W0221 519 if not self: 520 raise KeyError('set is empty') 521 key = self.end[1][0] if last else self.end[2][0] 522 self.discard(key) 523 return key 524 525 def __repr__(self): 526 if not self: 527 return '%s()' % (self.__class__.__name__,) 528 return '%s(%r)' % (self.__class__.__name__, list(self)) 529 530 def __eq__(self, other): 531 if isinstance(other, OrderedSet): 532 return len(self) == len(other) and list(self) == list(other) 533 return set(self) == set(other) 534 535 # Extensions to the recipe. 536 def update(self, iterable): 537 for i in iterable: 538 if i not in self: 539 self.add(i) 540 541 542class CycleError(Exception): 543 """An exception raised when an unexpected cycle is detected.""" 544 def __init__(self, nodes): 545 self.nodes = nodes 546 def __str__(self): 547 return 'CycleError: cycle involving: ' + str(self.nodes) 548 549 550def TopologicallySorted(graph, get_edges): 551 """Topologically sort based on a user provided edge definition. 552 553 Args: 554 graph: A list of node names. 555 get_edges: A function mapping from node name to a hashable collection 556 of node names which this node has outgoing edges to. 557 Returns: 558 A list containing all of the node in graph in topological order. 559 It is assumed that calling get_edges once for each node and caching is 560 cheaper than repeatedly calling get_edges. 561 Raises: 562 CycleError in the event of a cycle. 563 Example: 564 graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'} 565 def GetEdges(node): 566 return re.findall(r'\$\(([^))]\)', graph[node]) 567 print TopologicallySorted(graph.keys(), GetEdges) 568 ==> 569 ['a', 'c', b'] 570 """ 571 get_edges = memoize(get_edges) 572 visited = set() 573 visiting = set() 574 ordered_nodes = [] 575 def Visit(node): 576 if node in visiting: 577 raise CycleError(visiting) 578 if node in visited: 579 return 580 visited.add(node) 581 visiting.add(node) 582 for neighbor in get_edges(node): 583 Visit(neighbor) 584 visiting.remove(node) 585 ordered_nodes.insert(0, node) 586 for node in sorted(graph): 587 Visit(node) 588 return ordered_nodes 589