1#!/usr/bin/env python 2# encoding: utf-8 3# Thomas Nagy, 2005-2018 (ita) 4 5""" 6Tasks represent atomic operations such as processes. 7""" 8 9import os, re, sys, tempfile, traceback 10from waflib import Utils, Logs, Errors 11 12# task states 13NOT_RUN = 0 14"""The task was not executed yet""" 15 16MISSING = 1 17"""The task has been executed but the files have not been created""" 18 19CRASHED = 2 20"""The task execution returned a non-zero exit status""" 21 22EXCEPTION = 3 23"""An exception occurred in the task execution""" 24 25CANCELED = 4 26"""A dependency for the task is missing so it was cancelled""" 27 28SKIPPED = 8 29"""The task did not have to be executed""" 30 31SUCCESS = 9 32"""The task was successfully executed""" 33 34ASK_LATER = -1 35"""The task is not ready to be executed""" 36 37SKIP_ME = -2 38"""The task does not need to be executed""" 39 40RUN_ME = -3 41"""The task must be executed""" 42 43CANCEL_ME = -4 44"""The task cannot be executed because of a dependency problem""" 45 46COMPILE_TEMPLATE_SHELL = ''' 47def f(tsk): 48 env = tsk.env 49 gen = tsk.generator 50 bld = gen.bld 51 cwdx = tsk.get_cwd() 52 p = env.get_flat 53 def to_list(xx): 54 if isinstance(xx, str): return [xx] 55 return xx 56 tsk.last_cmd = cmd = \'\'\' %s \'\'\' % s 57 return tsk.exec_command(cmd, cwd=cwdx, env=env.env or None) 58''' 59 60COMPILE_TEMPLATE_NOSHELL = ''' 61def f(tsk): 62 env = tsk.env 63 gen = tsk.generator 64 bld = gen.bld 65 cwdx = tsk.get_cwd() 66 def to_list(xx): 67 if isinstance(xx, str): return [xx] 68 return xx 69 def merge(lst1, lst2): 70 if lst1 and lst2: 71 return lst1[:-1] + [lst1[-1] + lst2[0]] + lst2[1:] 72 return lst1 + lst2 73 lst = [] 74 %s 75 if '' in lst: 76 lst = [x for x in lst if x] 77 tsk.last_cmd = lst 78 return tsk.exec_command(lst, cwd=cwdx, env=env.env or None) 79''' 80 81COMPILE_TEMPLATE_SIG_VARS = ''' 82def f(tsk): 83 sig = tsk.generator.bld.hash_env_vars(tsk.env, tsk.vars) 84 tsk.m.update(sig) 85 env = tsk.env 86 gen = tsk.generator 87 bld = gen.bld 88 cwdx = tsk.get_cwd() 89 p = env.get_flat 90 buf = [] 91 %s 92 tsk.m.update(repr(buf).encode()) 93''' 94 95classes = {} 96""" 97The metaclass :py:class:`waflib.Task.store_task_type` stores all class tasks 98created by user scripts or Waf tools to this dict. It maps class names to class objects. 99""" 100 101class store_task_type(type): 102 """ 103 Metaclass: store the task classes into the dict pointed by the 104 class attribute 'register' which defaults to :py:const:`waflib.Task.classes`, 105 106 The attribute 'run_str' is compiled into a method 'run' bound to the task class. 107 """ 108 def __init__(cls, name, bases, dict): 109 super(store_task_type, cls).__init__(name, bases, dict) 110 name = cls.__name__ 111 112 if name != 'evil' and name != 'Task': 113 if getattr(cls, 'run_str', None): 114 # if a string is provided, convert it to a method 115 (f, dvars) = compile_fun(cls.run_str, cls.shell) 116 cls.hcode = Utils.h_cmd(cls.run_str) 117 cls.orig_run_str = cls.run_str 118 # change the name of run_str or it is impossible to subclass with a function 119 cls.run_str = None 120 cls.run = f 121 # process variables 122 cls.vars = list(set(cls.vars + dvars)) 123 cls.vars.sort() 124 if cls.vars: 125 fun = compile_sig_vars(cls.vars) 126 if fun: 127 cls.sig_vars = fun 128 elif getattr(cls, 'run', None) and not 'hcode' in cls.__dict__: 129 # getattr(cls, 'hcode') would look in the upper classes 130 cls.hcode = Utils.h_cmd(cls.run) 131 132 # be creative 133 getattr(cls, 'register', classes)[name] = cls 134 135evil = store_task_type('evil', (object,), {}) 136"Base class provided to avoid writing a metaclass, so the code can run in python 2.6 and 3.x unmodified" 137 138class Task(evil): 139 """ 140 Task objects represents actions to perform such as commands to execute by calling the `run` method. 141 142 Detecting when to execute a task occurs in the method :py:meth:`waflib.Task.Task.runnable_status`. 143 144 Detecting which tasks to execute is performed through a hash value returned by 145 :py:meth:`waflib.Task.Task.signature`. The task signature is persistent from build to build. 146 """ 147 vars = [] 148 """ConfigSet variables that should trigger a rebuild (class attribute used for :py:meth:`waflib.Task.Task.sig_vars`)""" 149 150 always_run = False 151 """Specify whether task instances must always be executed or not (class attribute)""" 152 153 shell = False 154 """Execute the command with the shell (class attribute)""" 155 156 color = 'GREEN' 157 """Color for the console display, see :py:const:`waflib.Logs.colors_lst`""" 158 159 ext_in = [] 160 """File extensions that objects of this task class may use""" 161 162 ext_out = [] 163 """File extensions that objects of this task class may create""" 164 165 before = [] 166 """The instances of this class are executed before the instances of classes whose names are in this list""" 167 168 after = [] 169 """The instances of this class are executed after the instances of classes whose names are in this list""" 170 171 hcode = Utils.SIG_NIL 172 """String representing an additional hash for the class representation""" 173 174 keep_last_cmd = False 175 """Whether to keep the last command executed on the instance after execution. 176 This may be useful for certain extensions but it can a lot of memory. 177 """ 178 179 weight = 0 180 """Optional weight to tune the priority for task instances. 181 The higher, the earlier. The weight only applies to single task objects.""" 182 183 tree_weight = 0 184 """Optional weight to tune the priority of task instances and whole subtrees. 185 The higher, the earlier.""" 186 187 prio_order = 0 188 """Priority order set by the scheduler on instances during the build phase. 189 You most likely do not need to set it. 190 """ 191 192 __slots__ = ('hasrun', 'generator', 'env', 'inputs', 'outputs', 'dep_nodes', 'run_after') 193 194 def __init__(self, *k, **kw): 195 self.hasrun = NOT_RUN 196 try: 197 self.generator = kw['generator'] 198 except KeyError: 199 self.generator = self 200 201 self.env = kw['env'] 202 """:py:class:`waflib.ConfigSet.ConfigSet` object (make sure to provide one)""" 203 204 self.inputs = [] 205 """List of input nodes, which represent the files used by the task instance""" 206 207 self.outputs = [] 208 """List of output nodes, which represent the files created by the task instance""" 209 210 self.dep_nodes = [] 211 """List of additional nodes to depend on""" 212 213 self.run_after = set() 214 """Set of tasks that must be executed before this one""" 215 216 def __lt__(self, other): 217 return self.priority() > other.priority() 218 def __le__(self, other): 219 return self.priority() >= other.priority() 220 def __gt__(self, other): 221 return self.priority() < other.priority() 222 def __ge__(self, other): 223 return self.priority() <= other.priority() 224 225 def get_cwd(self): 226 """ 227 :return: current working directory 228 :rtype: :py:class:`waflib.Node.Node` 229 """ 230 bld = self.generator.bld 231 ret = getattr(self, 'cwd', None) or getattr(bld, 'cwd', bld.bldnode) 232 if isinstance(ret, str): 233 if os.path.isabs(ret): 234 ret = bld.root.make_node(ret) 235 else: 236 ret = self.generator.path.make_node(ret) 237 return ret 238 239 def quote_flag(self, x): 240 """ 241 Surround a process argument by quotes so that a list of arguments can be written to a file 242 243 :param x: flag 244 :type x: string 245 :return: quoted flag 246 :rtype: string 247 """ 248 old = x 249 if '\\' in x: 250 x = x.replace('\\', '\\\\') 251 if '"' in x: 252 x = x.replace('"', '\\"') 253 if old != x or ' ' in x or '\t' in x or "'" in x: 254 x = '"%s"' % x 255 return x 256 257 def priority(self): 258 """ 259 Priority of execution; the higher, the earlier 260 261 :return: the priority value 262 :rtype: a tuple of numeric values 263 """ 264 return (self.weight + self.prio_order, - getattr(self.generator, 'tg_idx_count', 0)) 265 266 def split_argfile(self, cmd): 267 """ 268 Splits a list of process commands into the executable part and its list of arguments 269 270 :return: a tuple containing the executable first and then the rest of arguments 271 :rtype: tuple 272 """ 273 return ([cmd[0]], [self.quote_flag(x) for x in cmd[1:]]) 274 275 def exec_command(self, cmd, **kw): 276 """ 277 Wrapper for :py:meth:`waflib.Context.Context.exec_command`. 278 This version set the current working directory (``build.variant_dir``), 279 applies PATH settings (if self.env.PATH is provided), and can run long 280 commands through a temporary ``@argfile``. 281 282 :param cmd: process command to execute 283 :type cmd: list of string (best) or string (process will use a shell) 284 :return: the return code 285 :rtype: int 286 287 Optional parameters: 288 289 #. cwd: current working directory (Node or string) 290 #. stdout: set to None to prevent waf from capturing the process standard output 291 #. stderr: set to None to prevent waf from capturing the process standard error 292 #. timeout: timeout value (Python 3) 293 """ 294 if not 'cwd' in kw: 295 kw['cwd'] = self.get_cwd() 296 297 if hasattr(self, 'timeout'): 298 kw['timeout'] = self.timeout 299 300 if self.env.PATH: 301 env = kw['env'] = dict(kw.get('env') or self.env.env or os.environ) 302 env['PATH'] = self.env.PATH if isinstance(self.env.PATH, str) else os.pathsep.join(self.env.PATH) 303 304 if hasattr(self, 'stdout'): 305 kw['stdout'] = self.stdout 306 if hasattr(self, 'stderr'): 307 kw['stderr'] = self.stderr 308 309 if not isinstance(cmd, str): 310 if Utils.is_win32: 311 # win32 compares the resulting length http://support.microsoft.com/kb/830473 312 too_long = sum([len(arg) for arg in cmd]) + len(cmd) > 8192 313 else: 314 # non-win32 counts the amount of arguments (200k) 315 too_long = len(cmd) > 200000 316 317 if too_long and getattr(self, 'allow_argsfile', True): 318 # Shunt arguments to a temporary file if the command is too long. 319 cmd, args = self.split_argfile(cmd) 320 try: 321 (fd, tmp) = tempfile.mkstemp() 322 os.write(fd, '\r\n'.join(args).encode()) 323 os.close(fd) 324 if Logs.verbose: 325 Logs.debug('argfile: @%r -> %r', tmp, args) 326 return self.generator.bld.exec_command(cmd + ['@' + tmp], **kw) 327 finally: 328 try: 329 os.remove(tmp) 330 except OSError: 331 # anti-virus and indexers can keep files open -_- 332 pass 333 return self.generator.bld.exec_command(cmd, **kw) 334 335 def process(self): 336 """ 337 Runs the task and handles errors 338 339 :return: 0 or None if everything is fine 340 :rtype: integer 341 """ 342 # remove the task signature immediately before it is executed 343 # so that the task will be executed again in case of failure 344 try: 345 del self.generator.bld.task_sigs[self.uid()] 346 except KeyError: 347 pass 348 349 try: 350 ret = self.run() 351 except Exception: 352 self.err_msg = traceback.format_exc() 353 self.hasrun = EXCEPTION 354 else: 355 if ret: 356 self.err_code = ret 357 self.hasrun = CRASHED 358 else: 359 try: 360 self.post_run() 361 except Errors.WafError: 362 pass 363 except Exception: 364 self.err_msg = traceback.format_exc() 365 self.hasrun = EXCEPTION 366 else: 367 self.hasrun = SUCCESS 368 369 if self.hasrun != SUCCESS and self.scan: 370 # rescan dependencies on next run 371 try: 372 del self.generator.bld.imp_sigs[self.uid()] 373 except KeyError: 374 pass 375 376 def log_display(self, bld): 377 "Writes the execution status on the context logger" 378 if self.generator.bld.progress_bar == 3: 379 return 380 381 s = self.display() 382 if s: 383 if bld.logger: 384 logger = bld.logger 385 else: 386 logger = Logs 387 388 if self.generator.bld.progress_bar == 1: 389 c1 = Logs.colors.cursor_off 390 c2 = Logs.colors.cursor_on 391 logger.info(s, extra={'stream': sys.stderr, 'terminator':'', 'c1': c1, 'c2' : c2}) 392 else: 393 logger.info(s, extra={'terminator':'', 'c1': '', 'c2' : ''}) 394 395 def display(self): 396 """ 397 Returns an execution status for the console, the progress bar, or the IDE output. 398 399 :rtype: string 400 """ 401 col1 = Logs.colors(self.color) 402 col2 = Logs.colors.NORMAL 403 master = self.generator.bld.producer 404 405 def cur(): 406 # the current task position, computed as late as possible 407 return master.processed - master.ready.qsize() 408 409 if self.generator.bld.progress_bar == 1: 410 return self.generator.bld.progress_line(cur(), master.total, col1, col2) 411 412 if self.generator.bld.progress_bar == 2: 413 ela = str(self.generator.bld.timer) 414 try: 415 ins = ','.join([n.name for n in self.inputs]) 416 except AttributeError: 417 ins = '' 418 try: 419 outs = ','.join([n.name for n in self.outputs]) 420 except AttributeError: 421 outs = '' 422 return '|Total %s|Current %s|Inputs %s|Outputs %s|Time %s|\n' % (master.total, cur(), ins, outs, ela) 423 424 s = str(self) 425 if not s: 426 return None 427 428 total = master.total 429 n = len(str(total)) 430 fs = '[%%%dd/%%%dd] %%s%%s%%s%%s\n' % (n, n) 431 kw = self.keyword() 432 if kw: 433 kw += ' ' 434 return fs % (cur(), total, kw, col1, s, col2) 435 436 def hash_constraints(self): 437 """ 438 Identifies a task type for all the constraints relevant for the scheduler: precedence, file production 439 440 :return: a hash value 441 :rtype: string 442 """ 443 return (tuple(self.before), tuple(self.after), tuple(self.ext_in), tuple(self.ext_out), self.__class__.__name__, self.hcode) 444 445 def format_error(self): 446 """ 447 Returns an error message to display the build failure reasons 448 449 :rtype: string 450 """ 451 if Logs.verbose: 452 msg = ': %r\n%r' % (self, getattr(self, 'last_cmd', '')) 453 else: 454 msg = ' (run with -v to display more information)' 455 name = getattr(self.generator, 'name', '') 456 if getattr(self, "err_msg", None): 457 return self.err_msg 458 elif not self.hasrun: 459 return 'task in %r was not executed for some reason: %r' % (name, self) 460 elif self.hasrun == CRASHED: 461 try: 462 return ' -> task in %r failed with exit status %r%s' % (name, self.err_code, msg) 463 except AttributeError: 464 return ' -> task in %r failed%s' % (name, msg) 465 elif self.hasrun == MISSING: 466 return ' -> missing files in %r%s' % (name, msg) 467 elif self.hasrun == CANCELED: 468 return ' -> %r canceled because of missing dependencies' % name 469 else: 470 return 'invalid status for task in %r: %r' % (name, self.hasrun) 471 472 def colon(self, var1, var2): 473 """ 474 Enable scriptlet expressions of the form ${FOO_ST:FOO} 475 If the first variable (FOO_ST) is empty, then an empty list is returned 476 477 The results will be slightly different if FOO_ST is a list, for example:: 478 479 env.FOO = ['p1', 'p2'] 480 env.FOO_ST = '-I%s' 481 # ${FOO_ST:FOO} returns 482 ['-Ip1', '-Ip2'] 483 484 env.FOO_ST = ['-a', '-b'] 485 # ${FOO_ST:FOO} returns 486 ['-a', '-b', 'p1', '-a', '-b', 'p2'] 487 """ 488 tmp = self.env[var1] 489 if not tmp: 490 return [] 491 492 if isinstance(var2, str): 493 it = self.env[var2] 494 else: 495 it = var2 496 if isinstance(tmp, str): 497 return [tmp % x for x in it] 498 else: 499 lst = [] 500 for y in it: 501 lst.extend(tmp) 502 lst.append(y) 503 return lst 504 505 def __str__(self): 506 "string to display to the user" 507 name = self.__class__.__name__ 508 if self.outputs: 509 if name.endswith(('lib', 'program')) or not self.inputs: 510 node = self.outputs[0] 511 return node.path_from(node.ctx.launch_node()) 512 if not (self.inputs or self.outputs): 513 return self.__class__.__name__ 514 if len(self.inputs) == 1: 515 node = self.inputs[0] 516 return node.path_from(node.ctx.launch_node()) 517 518 src_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.inputs]) 519 tgt_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.outputs]) 520 if self.outputs: 521 sep = ' -> ' 522 else: 523 sep = '' 524 return '%s: %s%s%s' % (self.__class__.__name__, src_str, sep, tgt_str) 525 526 def keyword(self): 527 "Display keyword used to prettify the console outputs" 528 name = self.__class__.__name__ 529 if name.endswith(('lib', 'program')): 530 return 'Linking' 531 if len(self.inputs) == 1 and len(self.outputs) == 1: 532 return 'Compiling' 533 if not self.inputs: 534 if self.outputs: 535 return 'Creating' 536 else: 537 return 'Running' 538 return 'Processing' 539 540 def __repr__(self): 541 "for debugging purposes" 542 try: 543 ins = ",".join([x.name for x in self.inputs]) 544 outs = ",".join([x.name for x in self.outputs]) 545 except AttributeError: 546 ins = ",".join([str(x) for x in self.inputs]) 547 outs = ",".join([str(x) for x in self.outputs]) 548 return "".join(['\n\t{task %r: ' % id(self), self.__class__.__name__, " ", ins, " -> ", outs, '}']) 549 550 def uid(self): 551 """ 552 Returns an identifier used to determine if tasks are up-to-date. Since the 553 identifier will be stored between executions, it must be: 554 555 - unique for a task: no two tasks return the same value (for a given build context) 556 - the same for a given task instance 557 558 By default, the node paths, the class name, and the function are used 559 as inputs to compute a hash. 560 561 The pointer to the object (python built-in 'id') will change between build executions, 562 and must be avoided in such hashes. 563 564 :return: hash value 565 :rtype: string 566 """ 567 try: 568 return self.uid_ 569 except AttributeError: 570 m = Utils.md5(self.__class__.__name__) 571 up = m.update 572 for x in self.inputs + self.outputs: 573 up(x.abspath()) 574 self.uid_ = m.digest() 575 return self.uid_ 576 577 def set_inputs(self, inp): 578 """ 579 Appends the nodes to the *inputs* list 580 581 :param inp: input nodes 582 :type inp: node or list of nodes 583 """ 584 if isinstance(inp, list): 585 self.inputs += inp 586 else: 587 self.inputs.append(inp) 588 589 def set_outputs(self, out): 590 """ 591 Appends the nodes to the *outputs* list 592 593 :param out: output nodes 594 :type out: node or list of nodes 595 """ 596 if isinstance(out, list): 597 self.outputs += out 598 else: 599 self.outputs.append(out) 600 601 def set_run_after(self, task): 602 """ 603 Run this task only after the given *task*. 604 605 Calling this method from :py:meth:`waflib.Task.Task.runnable_status` may cause 606 build deadlocks; see :py:meth:`waflib.Tools.fc.fc.runnable_status` for details. 607 608 :param task: task 609 :type task: :py:class:`waflib.Task.Task` 610 """ 611 assert isinstance(task, Task) 612 self.run_after.add(task) 613 614 def signature(self): 615 """ 616 Task signatures are stored between build executions, they are use to track the changes 617 made to the input nodes (not to the outputs!). The signature hashes data from various sources: 618 619 * explicit dependencies: files listed in the inputs (list of node objects) :py:meth:`waflib.Task.Task.sig_explicit_deps` 620 * implicit dependencies: list of nodes returned by scanner methods (when present) :py:meth:`waflib.Task.Task.sig_implicit_deps` 621 * hashed data: variables/values read from task.vars/task.env :py:meth:`waflib.Task.Task.sig_vars` 622 623 If the signature is expected to give a different result, clear the cache kept in ``self.cache_sig``:: 624 625 from waflib import Task 626 class cls(Task.Task): 627 def signature(self): 628 sig = super(Task.Task, self).signature() 629 delattr(self, 'cache_sig') 630 return super(Task.Task, self).signature() 631 632 :return: the signature value 633 :rtype: string or bytes 634 """ 635 try: 636 return self.cache_sig 637 except AttributeError: 638 pass 639 640 self.m = Utils.md5(self.hcode) 641 642 # explicit deps 643 self.sig_explicit_deps() 644 645 # env vars 646 self.sig_vars() 647 648 # implicit deps / scanner results 649 if self.scan: 650 try: 651 self.sig_implicit_deps() 652 except Errors.TaskRescan: 653 return self.signature() 654 655 ret = self.cache_sig = self.m.digest() 656 return ret 657 658 def runnable_status(self): 659 """ 660 Returns the Task status 661 662 :return: a task state in :py:const:`waflib.Task.RUN_ME`, 663 :py:const:`waflib.Task.SKIP_ME`, :py:const:`waflib.Task.CANCEL_ME` or :py:const:`waflib.Task.ASK_LATER`. 664 :rtype: int 665 """ 666 bld = self.generator.bld 667 if bld.is_install < 0: 668 return SKIP_ME 669 670 for t in self.run_after: 671 if not t.hasrun: 672 return ASK_LATER 673 elif t.hasrun < SKIPPED: 674 # a dependency has an error 675 return CANCEL_ME 676 677 # first compute the signature 678 try: 679 new_sig = self.signature() 680 except Errors.TaskNotReady: 681 return ASK_LATER 682 683 # compare the signature to a signature computed previously 684 key = self.uid() 685 try: 686 prev_sig = bld.task_sigs[key] 687 except KeyError: 688 Logs.debug('task: task %r must run: it was never run before or the task code changed', self) 689 return RUN_ME 690 691 if new_sig != prev_sig: 692 Logs.debug('task: task %r must run: the task signature changed', self) 693 return RUN_ME 694 695 # compare the signatures of the outputs 696 for node in self.outputs: 697 sig = bld.node_sigs.get(node) 698 if not sig: 699 Logs.debug('task: task %r must run: an output node has no signature', self) 700 return RUN_ME 701 if sig != key: 702 Logs.debug('task: task %r must run: an output node was produced by another task', self) 703 return RUN_ME 704 if not node.exists(): 705 Logs.debug('task: task %r must run: an output node does not exist', self) 706 return RUN_ME 707 708 return (self.always_run and RUN_ME) or SKIP_ME 709 710 def post_run(self): 711 """ 712 Called after successful execution to record that the task has run by 713 updating the entry in :py:attr:`waflib.Build.BuildContext.task_sigs`. 714 """ 715 bld = self.generator.bld 716 for node in self.outputs: 717 if not node.exists(): 718 self.hasrun = MISSING 719 self.err_msg = '-> missing file: %r' % node.abspath() 720 raise Errors.WafError(self.err_msg) 721 bld.node_sigs[node] = self.uid() # make sure this task produced the files in question 722 bld.task_sigs[self.uid()] = self.signature() 723 if not self.keep_last_cmd: 724 try: 725 del self.last_cmd 726 except AttributeError: 727 pass 728 729 def sig_explicit_deps(self): 730 """ 731 Used by :py:meth:`waflib.Task.Task.signature`; it hashes :py:attr:`waflib.Task.Task.inputs` 732 and :py:attr:`waflib.Task.Task.dep_nodes` signatures. 733 """ 734 bld = self.generator.bld 735 upd = self.m.update 736 737 # the inputs 738 for x in self.inputs + self.dep_nodes: 739 upd(x.get_bld_sig()) 740 741 # manual dependencies, they can slow down the builds 742 if bld.deps_man: 743 additional_deps = bld.deps_man 744 for x in self.inputs + self.outputs: 745 try: 746 d = additional_deps[x] 747 except KeyError: 748 continue 749 750 for v in d: 751 try: 752 v = v.get_bld_sig() 753 except AttributeError: 754 if hasattr(v, '__call__'): 755 v = v() # dependency is a function, call it 756 upd(v) 757 758 def sig_deep_inputs(self): 759 """ 760 Enable rebuilds on input files task signatures. Not used by default. 761 762 Example: hashes of output programs can be unchanged after being re-linked, 763 despite the libraries being different. This method can thus prevent stale unit test 764 results (waf_unit_test.py). 765 766 Hashing input file timestamps is another possibility for the implementation. 767 This may cause unnecessary rebuilds when input tasks are frequently executed. 768 Here is an implementation example:: 769 770 lst = [] 771 for node in self.inputs + self.dep_nodes: 772 st = os.stat(node.abspath()) 773 lst.append(st.st_mtime) 774 lst.append(st.st_size) 775 self.m.update(Utils.h_list(lst)) 776 777 The downside of the implementation is that it absolutely requires all build directory 778 files to be declared within the current build. 779 """ 780 bld = self.generator.bld 781 lst = [bld.task_sigs[bld.node_sigs[node]] for node in (self.inputs + self.dep_nodes) if node.is_bld()] 782 self.m.update(Utils.h_list(lst)) 783 784 def sig_vars(self): 785 """ 786 Used by :py:meth:`waflib.Task.Task.signature`; it hashes :py:attr:`waflib.Task.Task.env` variables/values 787 When overriding this method, and if scriptlet expressions are used, make sure to follow 788 the code in :py:meth:`waflib.Task.Task.compile_sig_vars` to enable dependencies on scriptlet results. 789 790 This method may be replaced on subclasses by the metaclass to force dependencies on scriptlet code. 791 """ 792 sig = self.generator.bld.hash_env_vars(self.env, self.vars) 793 self.m.update(sig) 794 795 scan = None 796 """ 797 This method, when provided, returns a tuple containing: 798 799 * a list of nodes corresponding to real files 800 * a list of names for files not found in path_lst 801 802 For example:: 803 804 from waflib.Task import Task 805 class mytask(Task): 806 def scan(self, node): 807 return ([], []) 808 809 The first and second lists in the tuple are stored in :py:attr:`waflib.Build.BuildContext.node_deps` and 810 :py:attr:`waflib.Build.BuildContext.raw_deps` respectively. 811 """ 812 813 def sig_implicit_deps(self): 814 """ 815 Used by :py:meth:`waflib.Task.Task.signature`; it hashes node signatures 816 obtained by scanning for dependencies (:py:meth:`waflib.Task.Task.scan`). 817 818 The exception :py:class:`waflib.Errors.TaskRescan` is thrown 819 when a file has changed. In this case, the method :py:meth:`waflib.Task.Task.signature` is called 820 once again, and return here to call :py:meth:`waflib.Task.Task.scan` and searching for dependencies. 821 """ 822 bld = self.generator.bld 823 824 # get the task signatures from previous runs 825 key = self.uid() 826 prev = bld.imp_sigs.get(key, []) 827 828 # for issue #379 829 if prev: 830 try: 831 if prev == self.compute_sig_implicit_deps(): 832 return prev 833 except Errors.TaskNotReady: 834 raise 835 except EnvironmentError: 836 # when a file was renamed, remove the stale nodes (headers in folders without source files) 837 # this will break the order calculation for headers created during the build in the source directory (should be uncommon) 838 # the behaviour will differ when top != out 839 for x in bld.node_deps.get(self.uid(), []): 840 if not x.is_bld() and not x.exists(): 841 try: 842 del x.parent.children[x.name] 843 except KeyError: 844 pass 845 del bld.imp_sigs[key] 846 raise Errors.TaskRescan('rescan') 847 848 # no previous run or the signature of the dependencies has changed, rescan the dependencies 849 (bld.node_deps[key], bld.raw_deps[key]) = self.scan() 850 if Logs.verbose: 851 Logs.debug('deps: scanner for %s: %r; unresolved: %r', self, bld.node_deps[key], bld.raw_deps[key]) 852 853 # recompute the signature and return it 854 try: 855 bld.imp_sigs[key] = self.compute_sig_implicit_deps() 856 except EnvironmentError: 857 for k in bld.node_deps.get(self.uid(), []): 858 if not k.exists(): 859 Logs.warn('Dependency %r for %r is missing: check the task declaration and the build order!', k, self) 860 raise 861 862 def compute_sig_implicit_deps(self): 863 """ 864 Used by :py:meth:`waflib.Task.Task.sig_implicit_deps` for computing the actual hash of the 865 :py:class:`waflib.Node.Node` returned by the scanner. 866 867 :return: a hash value for the implicit dependencies 868 :rtype: string or bytes 869 """ 870 upd = self.m.update 871 self.are_implicit_nodes_ready() 872 873 # scanner returns a node that does not have a signature 874 # just *ignore* the error and let them figure out from the compiler output 875 # waf -k behaviour 876 for k in self.generator.bld.node_deps.get(self.uid(), []): 877 upd(k.get_bld_sig()) 878 return self.m.digest() 879 880 def are_implicit_nodes_ready(self): 881 """ 882 For each node returned by the scanner, see if there is a task that creates it, 883 and infer the build order 884 885 This has a low performance impact on null builds (1.86s->1.66s) thanks to caching (28s->1.86s) 886 """ 887 bld = self.generator.bld 888 try: 889 cache = bld.dct_implicit_nodes 890 except AttributeError: 891 bld.dct_implicit_nodes = cache = {} 892 893 # one cache per build group 894 try: 895 dct = cache[bld.current_group] 896 except KeyError: 897 dct = cache[bld.current_group] = {} 898 for tsk in bld.cur_tasks: 899 for x in tsk.outputs: 900 dct[x] = tsk 901 902 modified = False 903 for x in bld.node_deps.get(self.uid(), []): 904 if x in dct: 905 self.run_after.add(dct[x]) 906 modified = True 907 908 if modified: 909 for tsk in self.run_after: 910 if not tsk.hasrun: 911 #print "task is not ready..." 912 raise Errors.TaskNotReady('not ready') 913if sys.hexversion > 0x3000000: 914 def uid(self): 915 try: 916 return self.uid_ 917 except AttributeError: 918 m = Utils.md5(self.__class__.__name__.encode('latin-1', 'xmlcharrefreplace')) 919 up = m.update 920 for x in self.inputs + self.outputs: 921 up(x.abspath().encode('latin-1', 'xmlcharrefreplace')) 922 self.uid_ = m.digest() 923 return self.uid_ 924 uid.__doc__ = Task.uid.__doc__ 925 Task.uid = uid 926 927def is_before(t1, t2): 928 """ 929 Returns a non-zero value if task t1 is to be executed before task t2:: 930 931 t1.ext_out = '.h' 932 t2.ext_in = '.h' 933 t2.after = ['t1'] 934 t1.before = ['t2'] 935 waflib.Task.is_before(t1, t2) # True 936 937 :param t1: Task object 938 :type t1: :py:class:`waflib.Task.Task` 939 :param t2: Task object 940 :type t2: :py:class:`waflib.Task.Task` 941 """ 942 to_list = Utils.to_list 943 for k in to_list(t2.ext_in): 944 if k in to_list(t1.ext_out): 945 return 1 946 947 if t1.__class__.__name__ in to_list(t2.after): 948 return 1 949 950 if t2.__class__.__name__ in to_list(t1.before): 951 return 1 952 953 return 0 954 955def set_file_constraints(tasks): 956 """ 957 Updates the ``run_after`` attribute of all tasks based on the task inputs and outputs 958 959 :param tasks: tasks 960 :type tasks: list of :py:class:`waflib.Task.Task` 961 """ 962 ins = Utils.defaultdict(set) 963 outs = Utils.defaultdict(set) 964 for x in tasks: 965 for a in x.inputs: 966 ins[a].add(x) 967 for a in x.dep_nodes: 968 ins[a].add(x) 969 for a in x.outputs: 970 outs[a].add(x) 971 972 links = set(ins.keys()).intersection(outs.keys()) 973 for k in links: 974 for a in ins[k]: 975 a.run_after.update(outs[k]) 976 977 978class TaskGroup(object): 979 """ 980 Wrap nxm task order constraints into a single object 981 to prevent the creation of large list/set objects 982 983 This is an optimization 984 """ 985 def __init__(self, prev, next): 986 self.prev = prev 987 self.next = next 988 self.done = False 989 990 def get_hasrun(self): 991 for k in self.prev: 992 if not k.hasrun: 993 return NOT_RUN 994 return SUCCESS 995 996 hasrun = property(get_hasrun, None) 997 998def set_precedence_constraints(tasks): 999 """ 1000 Updates the ``run_after`` attribute of all tasks based on the after/before/ext_out/ext_in attributes 1001 1002 :param tasks: tasks 1003 :type tasks: list of :py:class:`waflib.Task.Task` 1004 """ 1005 cstr_groups = Utils.defaultdict(list) 1006 for x in tasks: 1007 h = x.hash_constraints() 1008 cstr_groups[h].append(x) 1009 1010 keys = list(cstr_groups.keys()) 1011 maxi = len(keys) 1012 1013 # this list should be short 1014 for i in range(maxi): 1015 t1 = cstr_groups[keys[i]][0] 1016 for j in range(i + 1, maxi): 1017 t2 = cstr_groups[keys[j]][0] 1018 1019 # add the constraints based on the comparisons 1020 if is_before(t1, t2): 1021 a = i 1022 b = j 1023 elif is_before(t2, t1): 1024 a = j 1025 b = i 1026 else: 1027 continue 1028 1029 a = cstr_groups[keys[a]] 1030 b = cstr_groups[keys[b]] 1031 1032 if len(a) < 2 or len(b) < 2: 1033 for x in b: 1034 x.run_after.update(a) 1035 else: 1036 group = TaskGroup(set(a), set(b)) 1037 for x in b: 1038 x.run_after.add(group) 1039 1040def funex(c): 1041 """ 1042 Compiles a scriptlet expression into a Python function 1043 1044 :param c: function to compile 1045 :type c: string 1046 :return: the function 'f' declared in the input string 1047 :rtype: function 1048 """ 1049 dc = {} 1050 exec(c, dc) 1051 return dc['f'] 1052 1053re_cond = re.compile(r'(?P<var>\w+)|(?P<or>\|)|(?P<and>&)') 1054re_novar = re.compile(r'^(SRC|TGT)\W+.*?$') 1055reg_act = re.compile(r'(?P<backslash>\\)|(?P<dollar>\$\$)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})', re.M) 1056def compile_fun_shell(line): 1057 """ 1058 Creates a compiled function to execute a process through a sub-shell 1059 """ 1060 extr = [] 1061 def repl(match): 1062 g = match.group 1063 if g('dollar'): 1064 return "$" 1065 elif g('backslash'): 1066 return '\\\\' 1067 elif g('subst'): 1068 extr.append((g('var'), g('code'))) 1069 return "%s" 1070 return None 1071 line = reg_act.sub(repl, line) or line 1072 dvars = [] 1073 def add_dvar(x): 1074 if x not in dvars: 1075 dvars.append(x) 1076 1077 def replc(m): 1078 # performs substitutions and populates dvars 1079 if m.group('and'): 1080 return ' and ' 1081 elif m.group('or'): 1082 return ' or ' 1083 else: 1084 x = m.group('var') 1085 add_dvar(x) 1086 return 'env[%r]' % x 1087 1088 parm = [] 1089 app = parm.append 1090 for (var, meth) in extr: 1091 if var == 'SRC': 1092 if meth: 1093 app('tsk.inputs%s' % meth) 1094 else: 1095 app('" ".join([a.path_from(cwdx) for a in tsk.inputs])') 1096 elif var == 'TGT': 1097 if meth: 1098 app('tsk.outputs%s' % meth) 1099 else: 1100 app('" ".join([a.path_from(cwdx) for a in tsk.outputs])') 1101 elif meth: 1102 if meth.startswith(':'): 1103 add_dvar(var) 1104 m = meth[1:] 1105 if m == 'SRC': 1106 m = '[a.path_from(cwdx) for a in tsk.inputs]' 1107 elif m == 'TGT': 1108 m = '[a.path_from(cwdx) for a in tsk.outputs]' 1109 elif re_novar.match(m): 1110 m = '[tsk.inputs%s]' % m[3:] 1111 elif re_novar.match(m): 1112 m = '[tsk.outputs%s]' % m[3:] 1113 else: 1114 add_dvar(m) 1115 if m[:3] not in ('tsk', 'gen', 'bld'): 1116 m = '%r' % m 1117 app('" ".join(tsk.colon(%r, %s))' % (var, m)) 1118 elif meth.startswith('?'): 1119 # In A?B|C output env.A if one of env.B or env.C is non-empty 1120 expr = re_cond.sub(replc, meth[1:]) 1121 app('p(%r) if (%s) else ""' % (var, expr)) 1122 else: 1123 call = '%s%s' % (var, meth) 1124 add_dvar(call) 1125 app(call) 1126 else: 1127 add_dvar(var) 1128 app("p('%s')" % var) 1129 if parm: 1130 parm = "%% (%s) " % (',\n\t\t'.join(parm)) 1131 else: 1132 parm = '' 1133 1134 c = COMPILE_TEMPLATE_SHELL % (line, parm) 1135 Logs.debug('action: %s', c.strip().splitlines()) 1136 return (funex(c), dvars) 1137 1138reg_act_noshell = re.compile(r"(?P<space>\s+)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})|(?P<text>([^$ \t\n\r\f\v]|\$\$)+)", re.M) 1139def compile_fun_noshell(line): 1140 """ 1141 Creates a compiled function to execute a process without a sub-shell 1142 """ 1143 buf = [] 1144 dvars = [] 1145 merge = False 1146 app = buf.append 1147 1148 def add_dvar(x): 1149 if x not in dvars: 1150 dvars.append(x) 1151 1152 def replc(m): 1153 # performs substitutions and populates dvars 1154 if m.group('and'): 1155 return ' and ' 1156 elif m.group('or'): 1157 return ' or ' 1158 else: 1159 x = m.group('var') 1160 add_dvar(x) 1161 return 'env[%r]' % x 1162 1163 for m in reg_act_noshell.finditer(line): 1164 if m.group('space'): 1165 merge = False 1166 continue 1167 elif m.group('text'): 1168 app('[%r]' % m.group('text').replace('$$', '$')) 1169 elif m.group('subst'): 1170 var = m.group('var') 1171 code = m.group('code') 1172 if var == 'SRC': 1173 if code: 1174 app('[tsk.inputs%s]' % code) 1175 else: 1176 app('[a.path_from(cwdx) for a in tsk.inputs]') 1177 elif var == 'TGT': 1178 if code: 1179 app('[tsk.outputs%s]' % code) 1180 else: 1181 app('[a.path_from(cwdx) for a in tsk.outputs]') 1182 elif code: 1183 if code.startswith(':'): 1184 # a composed variable ${FOO:OUT} 1185 add_dvar(var) 1186 m = code[1:] 1187 if m == 'SRC': 1188 m = '[a.path_from(cwdx) for a in tsk.inputs]' 1189 elif m == 'TGT': 1190 m = '[a.path_from(cwdx) for a in tsk.outputs]' 1191 elif re_novar.match(m): 1192 m = '[tsk.inputs%s]' % m[3:] 1193 elif re_novar.match(m): 1194 m = '[tsk.outputs%s]' % m[3:] 1195 else: 1196 add_dvar(m) 1197 if m[:3] not in ('tsk', 'gen', 'bld'): 1198 m = '%r' % m 1199 app('tsk.colon(%r, %s)' % (var, m)) 1200 elif code.startswith('?'): 1201 # In A?B|C output env.A if one of env.B or env.C is non-empty 1202 expr = re_cond.sub(replc, code[1:]) 1203 app('to_list(env[%r] if (%s) else [])' % (var, expr)) 1204 else: 1205 # plain code such as ${tsk.inputs[0].abspath()} 1206 call = '%s%s' % (var, code) 1207 add_dvar(call) 1208 app('to_list(%s)' % call) 1209 else: 1210 # a plain variable such as # a plain variable like ${AR} 1211 app('to_list(env[%r])' % var) 1212 add_dvar(var) 1213 if merge: 1214 tmp = 'merge(%s, %s)' % (buf[-2], buf[-1]) 1215 del buf[-1] 1216 buf[-1] = tmp 1217 merge = True # next turn 1218 1219 buf = ['lst.extend(%s)' % x for x in buf] 1220 fun = COMPILE_TEMPLATE_NOSHELL % "\n\t".join(buf) 1221 Logs.debug('action: %s', fun.strip().splitlines()) 1222 return (funex(fun), dvars) 1223 1224def compile_fun(line, shell=False): 1225 """ 1226 Parses a string expression such as '${CC} ${SRC} -o ${TGT}' and returns a pair containing: 1227 1228 * The function created (compiled) for use as :py:meth:`waflib.Task.Task.run` 1229 * The list of variables that must cause rebuilds when *env* data is modified 1230 1231 for example:: 1232 1233 from waflib.Task import compile_fun 1234 compile_fun('cxx', '${CXX} -o ${TGT[0]} ${SRC} -I ${SRC[0].parent.bldpath()}') 1235 1236 def build(bld): 1237 bld(source='wscript', rule='echo "foo\\${SRC[0].name}\\bar"') 1238 1239 The env variables (CXX, ..) on the task must not hold dicts so as to preserve a consistent order. 1240 The reserved keywords ``TGT`` and ``SRC`` represent the task input and output nodes 1241 1242 """ 1243 if isinstance(line, str): 1244 if line.find('<') > 0 or line.find('>') > 0 or line.find('&&') > 0: 1245 shell = True 1246 else: 1247 dvars_lst = [] 1248 funs_lst = [] 1249 for x in line: 1250 if isinstance(x, str): 1251 fun, dvars = compile_fun(x, shell) 1252 dvars_lst += dvars 1253 funs_lst.append(fun) 1254 else: 1255 # assume a function to let through 1256 funs_lst.append(x) 1257 def composed_fun(task): 1258 for x in funs_lst: 1259 ret = x(task) 1260 if ret: 1261 return ret 1262 return None 1263 return composed_fun, dvars_lst 1264 if shell: 1265 return compile_fun_shell(line) 1266 else: 1267 return compile_fun_noshell(line) 1268 1269def compile_sig_vars(vars): 1270 """ 1271 This method produces a sig_vars method suitable for subclasses that provide 1272 scriptlet code in their run_str code. 1273 If no such method can be created, this method returns None. 1274 1275 The purpose of the sig_vars method returned is to ensures 1276 that rebuilds occur whenever the contents of the expression changes. 1277 This is the case B below:: 1278 1279 import time 1280 # case A: regular variables 1281 tg = bld(rule='echo ${FOO}') 1282 tg.env.FOO = '%s' % time.time() 1283 # case B 1284 bld(rule='echo ${gen.foo}', foo='%s' % time.time()) 1285 1286 :param vars: env variables such as CXXFLAGS or gen.foo 1287 :type vars: list of string 1288 :return: A sig_vars method relevant for dependencies if adequate, else None 1289 :rtype: A function, or None in most cases 1290 """ 1291 buf = [] 1292 for x in sorted(vars): 1293 if x[:3] in ('tsk', 'gen', 'bld'): 1294 buf.append('buf.append(%s)' % x) 1295 if buf: 1296 return funex(COMPILE_TEMPLATE_SIG_VARS % '\n\t'.join(buf)) 1297 return None 1298 1299def task_factory(name, func=None, vars=None, color='GREEN', ext_in=[], ext_out=[], before=[], after=[], shell=False, scan=None): 1300 """ 1301 Returns a new task subclass with the function ``run`` compiled from the line given. 1302 1303 :param func: method run 1304 :type func: string or function 1305 :param vars: list of variables to hash 1306 :type vars: list of string 1307 :param color: color to use 1308 :type color: string 1309 :param shell: when *func* is a string, enable/disable the use of the shell 1310 :type shell: bool 1311 :param scan: method scan 1312 :type scan: function 1313 :rtype: :py:class:`waflib.Task.Task` 1314 """ 1315 1316 params = { 1317 'vars': vars or [], # function arguments are static, and this one may be modified by the class 1318 'color': color, 1319 'name': name, 1320 'shell': shell, 1321 'scan': scan, 1322 } 1323 1324 if isinstance(func, str) or isinstance(func, tuple): 1325 params['run_str'] = func 1326 else: 1327 params['run'] = func 1328 1329 cls = type(Task)(name, (Task,), params) 1330 classes[name] = cls 1331 1332 if ext_in: 1333 cls.ext_in = Utils.to_list(ext_in) 1334 if ext_out: 1335 cls.ext_out = Utils.to_list(ext_out) 1336 if before: 1337 cls.before = Utils.to_list(before) 1338 if after: 1339 cls.after = Utils.to_list(after) 1340 1341 return cls 1342 1343def deep_inputs(cls): 1344 """ 1345 Task class decorator to enable rebuilds on input files task signatures 1346 """ 1347 def sig_explicit_deps(self): 1348 Task.sig_explicit_deps(self) 1349 Task.sig_deep_inputs(self) 1350 cls.sig_explicit_deps = sig_explicit_deps 1351 return cls 1352 1353TaskBase = Task 1354"Provided for compatibility reasons, TaskBase should not be used" 1355 1356class TaskSemaphore(object): 1357 """ 1358 Task semaphores provide a simple and efficient way of throttling the amount of 1359 a particular task to run concurrently. The throttling value is capped 1360 by the amount of maximum jobs, so for example, a `TaskSemaphore(10)` 1361 has no effect in a `-j2` build. 1362 1363 Task semaphores are typically specified on the task class level:: 1364 1365 class compile(waflib.Task.Task): 1366 semaphore = waflib.Task.TaskSemaphore(2) 1367 run_str = 'touch ${TGT}' 1368 1369 Task semaphores are meant to be used by the build scheduler in the main 1370 thread, so there are no guarantees of thread safety. 1371 """ 1372 def __init__(self, num): 1373 """ 1374 :param num: maximum value of concurrent tasks 1375 :type num: int 1376 """ 1377 self.num = num 1378 self.locking = set() 1379 self.waiting = set() 1380 1381 def is_locked(self): 1382 """Returns True if this semaphore cannot be acquired by more tasks""" 1383 return len(self.locking) >= self.num 1384 1385 def acquire(self, tsk): 1386 """ 1387 Mark the semaphore as used by the given task (not re-entrant). 1388 1389 :param tsk: task object 1390 :type tsk: :py:class:`waflib.Task.Task` 1391 :raises: :py:class:`IndexError` in case the resource is already acquired 1392 """ 1393 if self.is_locked(): 1394 raise IndexError('Cannot lock more %r' % self.locking) 1395 self.locking.add(tsk) 1396 1397 def release(self, tsk): 1398 """ 1399 Mark the semaphore as unused by the given task. 1400 1401 :param tsk: task object 1402 :type tsk: :py:class:`waflib.Task.Task` 1403 :raises: :py:class:`KeyError` in case the resource is not acquired by the task 1404 """ 1405 self.locking.remove(tsk) 1406 1407