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