1# MIT License 2# 3# Copyright The SCons Foundation 4# 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, 9# distribute, sublicense, and/or sell copies of the Software, and to 10# permit persons to whom the Software is furnished to do so, subject to 11# the following conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 17# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 24"""Execute actions with specific lists of target and source Nodes.""" 25 26import collections 27 28import SCons.Errors 29import SCons.Memoize 30import SCons.Util 31from SCons.compat import NoSlotsPyPy 32import SCons.Debug 33from SCons.Debug import logInstanceCreation 34 35class Batch: 36 """Remembers exact association between targets 37 and sources of executor.""" 38 39 __slots__ = ('targets', 40 'sources') 41 42 def __init__(self, targets=[], sources=[]): 43 self.targets = targets 44 self.sources = sources 45 46 47 48class TSList(collections.UserList): 49 """A class that implements $TARGETS or $SOURCES expansions by wrapping 50 an executor Method. This class is used in the Executor.lvars() 51 to delay creation of NodeList objects until they're needed. 52 53 Note that we subclass collections.UserList purely so that the 54 is_Sequence() function will identify an object of this class as 55 a list during variable expansion. We're not really using any 56 collections.UserList methods in practice. 57 """ 58 def __init__(self, func): 59 self.func = func 60 def __getattr__(self, attr): 61 nl = self.func() 62 return getattr(nl, attr) 63 def __getitem__(self, i): 64 nl = self.func() 65 return nl[i] 66 def __str__(self): 67 nl = self.func() 68 return str(nl) 69 def __repr__(self): 70 nl = self.func() 71 return repr(nl) 72 73class TSObject: 74 """A class that implements $TARGET or $SOURCE expansions by wrapping 75 an Executor method. 76 """ 77 def __init__(self, func): 78 self.func = func 79 def __getattr__(self, attr): 80 n = self.func() 81 return getattr(n, attr) 82 def __str__(self): 83 n = self.func() 84 if n: 85 return str(n) 86 return '' 87 def __repr__(self): 88 n = self.func() 89 if n: 90 return repr(n) 91 return '' 92 93def rfile(node): 94 """ 95 A function to return the results of a Node's rfile() method, 96 if it exists, and the Node itself otherwise (if it's a Value 97 Node, e.g.). 98 """ 99 try: 100 rfile = node.rfile 101 except AttributeError: 102 return node 103 else: 104 return rfile() 105 106 107def execute_nothing(obj, target, kw): 108 return 0 109 110def execute_action_list(obj, target, kw): 111 """Actually execute the action list.""" 112 env = obj.get_build_env() 113 kw = obj.get_kw(kw) 114 status = 0 115 for act in obj.get_action_list(): 116 args = ([], [], env) 117 status = act(*args, **kw) 118 if isinstance(status, SCons.Errors.BuildError): 119 status.executor = obj 120 raise status # TODO pylint E0702: raising int not allowed 121 elif status: 122 msg = "Error %s" % status 123 raise SCons.Errors.BuildError( 124 errstr=msg, 125 node=obj.batches[0].targets, 126 executor=obj, 127 action=act) 128 return status 129 130_do_execute_map = {0 : execute_nothing, 131 1 : execute_action_list} 132 133 134def execute_actions_str(obj): 135 env = obj.get_build_env() 136 return "\n".join([action.genstring(obj.get_all_targets(), 137 obj.get_all_sources(), 138 env) 139 for action in obj.get_action_list()]) 140 141def execute_null_str(obj): 142 return '' 143 144_execute_str_map = {0 : execute_null_str, 145 1 : execute_actions_str} 146 147 148class Executor(object, metaclass=NoSlotsPyPy): 149 """A class for controlling instances of executing an action. 150 151 This largely exists to hold a single association of an action, 152 environment, list of environment override dictionaries, targets 153 and sources for later processing as needed. 154 """ 155 156 __slots__ = ('pre_actions', 157 'post_actions', 158 'env', 159 'overridelist', 160 'batches', 161 'builder_kw', 162 '_memo', 163 'lvars', 164 '_changed_sources_list', 165 '_changed_targets_list', 166 '_unchanged_sources_list', 167 '_unchanged_targets_list', 168 'action_list', 169 '_do_execute', 170 '_execute_str') 171 172 def __init__(self, action, env=None, overridelist=[{}], 173 targets=[], sources=[], builder_kw={}): 174 if SCons.Debug.track_instances: logInstanceCreation(self, 'Executor.Executor') 175 self.set_action_list(action) 176 self.pre_actions = [] 177 self.post_actions = [] 178 self.env = env 179 self.overridelist = overridelist 180 if targets or sources: 181 self.batches = [Batch(targets[:], sources[:])] 182 else: 183 self.batches = [] 184 self.builder_kw = builder_kw 185 self._do_execute = 1 186 self._execute_str = 1 187 self._memo = {} 188 189 def get_lvars(self): 190 try: 191 return self.lvars 192 except AttributeError: 193 self.lvars = { 194 'CHANGED_SOURCES' : TSList(self._get_changed_sources), 195 'CHANGED_TARGETS' : TSList(self._get_changed_targets), 196 'SOURCE' : TSObject(self._get_source), 197 'SOURCES' : TSList(self._get_sources), 198 'TARGET' : TSObject(self._get_target), 199 'TARGETS' : TSList(self._get_targets), 200 'UNCHANGED_SOURCES' : TSList(self._get_unchanged_sources), 201 'UNCHANGED_TARGETS' : TSList(self._get_unchanged_targets), 202 } 203 return self.lvars 204 205 def _get_changes(self): 206 cs = [] 207 ct = [] 208 us = [] 209 ut = [] 210 for b in self.batches: 211 # don't add targets marked always build to unchanged lists 212 # add to changed list as they always need to build 213 if not b.targets[0].always_build and b.targets[0].is_up_to_date(): 214 us.extend(list(map(rfile, b.sources))) 215 ut.extend(b.targets) 216 else: 217 cs.extend(list(map(rfile, b.sources))) 218 ct.extend(b.targets) 219 self._changed_sources_list = SCons.Util.NodeList(cs) 220 self._changed_targets_list = SCons.Util.NodeList(ct) 221 self._unchanged_sources_list = SCons.Util.NodeList(us) 222 self._unchanged_targets_list = SCons.Util.NodeList(ut) 223 224 def _get_changed_sources(self, *args, **kw): 225 try: 226 return self._changed_sources_list 227 except AttributeError: 228 self._get_changes() 229 return self._changed_sources_list 230 231 def _get_changed_targets(self, *args, **kw): 232 try: 233 return self._changed_targets_list 234 except AttributeError: 235 self._get_changes() 236 return self._changed_targets_list 237 238 def _get_source(self, *args, **kw): 239 return rfile(self.batches[0].sources[0]).get_subst_proxy() 240 241 def _get_sources(self, *args, **kw): 242 return SCons.Util.NodeList([rfile(n).get_subst_proxy() for n in self.get_all_sources()]) 243 244 def _get_target(self, *args, **kw): 245 return self.batches[0].targets[0].get_subst_proxy() 246 247 def _get_targets(self, *args, **kw): 248 return SCons.Util.NodeList([n.get_subst_proxy() for n in self.get_all_targets()]) 249 250 def _get_unchanged_sources(self, *args, **kw): 251 try: 252 return self._unchanged_sources_list 253 except AttributeError: 254 self._get_changes() 255 return self._unchanged_sources_list 256 257 def _get_unchanged_targets(self, *args, **kw): 258 try: 259 return self._unchanged_targets_list 260 except AttributeError: 261 self._get_changes() 262 return self._unchanged_targets_list 263 264 def get_action_targets(self): 265 if not self.action_list: 266 return [] 267 targets_string = self.action_list[0].get_targets(self.env, self) 268 if targets_string[0] == '$': 269 targets_string = targets_string[1:] 270 return self.get_lvars()[targets_string] 271 272 def set_action_list(self, action): 273 if not SCons.Util.is_List(action): 274 if not action: 275 raise SCons.Errors.UserError("Executor must have an action.") 276 action = [action] 277 self.action_list = action 278 279 def get_action_list(self): 280 if self.action_list is None: 281 return [] 282 return self.pre_actions + self.action_list + self.post_actions 283 284 def get_all_targets(self): 285 """Returns all targets for all batches of this Executor.""" 286 result = [] 287 for batch in self.batches: 288 result.extend(batch.targets) 289 return result 290 291 def get_all_sources(self): 292 """Returns all sources for all batches of this Executor.""" 293 result = [] 294 for batch in self.batches: 295 result.extend(batch.sources) 296 return result 297 298 def get_all_children(self): 299 """Returns all unique children (dependencies) for all batches 300 of this Executor. 301 302 The Taskmaster can recognize when it's already evaluated a 303 Node, so we don't have to make this list unique for its intended 304 canonical use case, but we expect there to be a lot of redundancy 305 (long lists of batched .cc files #including the same .h files 306 over and over), so removing the duplicates once up front should 307 save the Taskmaster a lot of work. 308 """ 309 result = SCons.Util.UniqueList([]) 310 for target in self.get_all_targets(): 311 result.extend(target.children()) 312 return result 313 314 def get_all_prerequisites(self): 315 """Returns all unique (order-only) prerequisites for all batches 316 of this Executor. 317 """ 318 result = SCons.Util.UniqueList([]) 319 for target in self.get_all_targets(): 320 if target.prerequisites is not None: 321 result.extend(target.prerequisites) 322 return result 323 324 def get_action_side_effects(self): 325 326 """Returns all side effects for all batches of this 327 Executor used by the underlying Action. 328 """ 329 result = SCons.Util.UniqueList([]) 330 for target in self.get_action_targets(): 331 result.extend(target.side_effects) 332 return result 333 334 @SCons.Memoize.CountMethodCall 335 def get_build_env(self): 336 """Fetch or create the appropriate build Environment 337 for this Executor. 338 """ 339 try: 340 return self._memo['get_build_env'] 341 except KeyError: 342 pass 343 344 # Create the build environment instance with appropriate 345 # overrides. These get evaluated against the current 346 # environment's construction variables so that users can 347 # add to existing values by referencing the variable in 348 # the expansion. 349 overrides = {} 350 for odict in self.overridelist: 351 overrides.update(odict) 352 353 import SCons.Defaults 354 env = self.env or SCons.Defaults.DefaultEnvironment() 355 build_env = env.Override(overrides) 356 357 self._memo['get_build_env'] = build_env 358 359 return build_env 360 361 def get_build_scanner_path(self, scanner): 362 """Fetch the scanner path for this executor's targets and sources. 363 """ 364 env = self.get_build_env() 365 try: 366 cwd = self.batches[0].targets[0].cwd 367 except (IndexError, AttributeError): 368 cwd = None 369 return scanner.path(env, cwd, 370 self.get_all_targets(), 371 self.get_all_sources()) 372 373 def get_kw(self, kw={}): 374 result = self.builder_kw.copy() 375 result.update(kw) 376 result['executor'] = self 377 return result 378 379 # use extra indirection because with new-style objects (Python 2.2 380 # and above) we can't override special methods, and nullify() needs 381 # to be able to do this. 382 383 def __call__(self, target, **kw): 384 return _do_execute_map[self._do_execute](self, target, kw) 385 386 def cleanup(self): 387 self._memo = {} 388 389 def add_sources(self, sources): 390 """Add source files to this Executor's list. This is necessary 391 for "multi" Builders that can be called repeatedly to build up 392 a source file list for a given target.""" 393 # TODO(batch): extend to multiple batches 394 assert (len(self.batches) == 1) 395 # TODO(batch): remove duplicates? 396 sources = [x for x in sources if x not in self.batches[0].sources] 397 self.batches[0].sources.extend(sources) 398 399 def get_sources(self): 400 return self.batches[0].sources 401 402 def add_batch(self, targets, sources): 403 """Add pair of associated target and source to this Executor's list. 404 This is necessary for "batch" Builders that can be called repeatedly 405 to build up a list of matching target and source files that will be 406 used in order to update multiple target files at once from multiple 407 corresponding source files, for tools like MSVC that support it.""" 408 self.batches.append(Batch(targets, sources)) 409 410 def prepare(self): 411 """ 412 Preparatory checks for whether this Executor can go ahead 413 and (try to) build its targets. 414 """ 415 for s in self.get_all_sources(): 416 if s.missing(): 417 msg = "Source `%s' not found, needed by target `%s'." 418 raise SCons.Errors.StopError(msg % (s, self.batches[0].targets[0])) 419 420 def add_pre_action(self, action): 421 self.pre_actions.append(action) 422 423 def add_post_action(self, action): 424 self.post_actions.append(action) 425 426 # another extra indirection for new-style objects and nullify... 427 428 def __str__(self): 429 return _execute_str_map[self._execute_str](self) 430 431 def nullify(self): 432 self.cleanup() 433 self._do_execute = 0 434 self._execute_str = 0 435 436 @SCons.Memoize.CountMethodCall 437 def get_contents(self): 438 """Fetch the signature contents. This is the main reason this 439 class exists, so we can compute this once and cache it regardless 440 of how many target or source Nodes there are. 441 442 Returns bytes 443 """ 444 try: 445 return self._memo['get_contents'] 446 except KeyError: 447 pass 448 env = self.get_build_env() 449 450 action_list = self.get_action_list() 451 all_targets = self.get_all_targets() 452 all_sources = self.get_all_sources() 453 454 result = bytearray("",'utf-8').join([action.get_contents(all_targets, 455 all_sources, 456 env) 457 for action in action_list]) 458 459 self._memo['get_contents'] = result 460 return result 461 462 def get_timestamp(self): 463 """Fetch a time stamp for this Executor. We don't have one, of 464 course (only files do), but this is the interface used by the 465 timestamp module. 466 """ 467 return 0 468 469 def scan_targets(self, scanner): 470 # TODO(batch): scan by batches 471 self.scan(scanner, self.get_all_targets()) 472 473 def scan_sources(self, scanner): 474 # TODO(batch): scan by batches 475 if self.batches[0].sources: 476 self.scan(scanner, self.get_all_sources()) 477 478 def scan(self, scanner, node_list): 479 """Scan a list of this Executor's files (targets or sources) for 480 implicit dependencies and update all of the targets with them. 481 This essentially short-circuits an N*M scan of the sources for 482 each individual target, which is a hell of a lot more efficient. 483 """ 484 env = self.get_build_env() 485 path = self.get_build_scanner_path 486 kw = self.get_kw() 487 488 # TODO(batch): scan by batches) 489 deps = [] 490 491 for node in node_list: 492 node.disambiguate() 493 deps.extend(node.get_implicit_deps(env, scanner, path, kw)) 494 495 deps.extend(self.get_implicit_deps()) 496 497 for tgt in self.get_all_targets(): 498 tgt.add_to_implicit(deps) 499 500 def _get_unignored_sources_key(self, node, ignore=()): 501 return (node,) + tuple(ignore) 502 503 @SCons.Memoize.CountDictCall(_get_unignored_sources_key) 504 def get_unignored_sources(self, node, ignore=()): 505 key = (node,) + tuple(ignore) 506 try: 507 memo_dict = self._memo['get_unignored_sources'] 508 except KeyError: 509 memo_dict = {} 510 self._memo['get_unignored_sources'] = memo_dict 511 else: 512 try: 513 return memo_dict[key] 514 except KeyError: 515 pass 516 517 if node: 518 # TODO: better way to do this (it's a linear search, 519 # but it may not be critical path)? 520 sourcelist = [] 521 for b in self.batches: 522 if node in b.targets: 523 sourcelist = b.sources 524 break 525 else: 526 sourcelist = self.get_all_sources() 527 if ignore: 528 idict = {} 529 for i in ignore: 530 idict[i] = 1 531 sourcelist = [s for s in sourcelist if s not in idict] 532 533 memo_dict[key] = sourcelist 534 535 return sourcelist 536 537 def get_implicit_deps(self): 538 """Return the executor's implicit dependencies, i.e. the nodes of 539 the commands to be executed.""" 540 result = [] 541 build_env = self.get_build_env() 542 for act in self.get_action_list(): 543 deps = act.get_implicit_deps(self.get_all_targets(), 544 self.get_all_sources(), 545 build_env) 546 result.extend(deps) 547 return result 548 549 550 551_batch_executors = {} 552 553def GetBatchExecutor(key): 554 return _batch_executors[key] 555 556def AddBatchExecutor(key, executor): 557 assert key not in _batch_executors 558 _batch_executors[key] = executor 559 560nullenv = None 561 562 563class NullEnvironment(SCons.Util.Null): 564 import SCons.CacheDir 565 _CacheDir_path = None 566 _CacheDir = SCons.CacheDir.CacheDir(None) 567 def get_CacheDir(self): 568 return self._CacheDir 569 570 571def get_NullEnvironment(): 572 """Use singleton pattern for Null Environments.""" 573 global nullenv 574 575 if nullenv is None: 576 nullenv = NullEnvironment() 577 return nullenv 578 579class Null(object, metaclass=NoSlotsPyPy): 580 """A null Executor, with a null build Environment, that does 581 nothing when the rest of the methods call it. 582 583 This might be able to disappear when we refactor things to 584 disassociate Builders from Nodes entirely, so we're not 585 going to worry about unit tests for this--at least for now. 586 """ 587 588 __slots__ = ('pre_actions', 589 'post_actions', 590 'env', 591 'overridelist', 592 'batches', 593 'builder_kw', 594 '_memo', 595 'lvars', 596 '_changed_sources_list', 597 '_changed_targets_list', 598 '_unchanged_sources_list', 599 '_unchanged_targets_list', 600 'action_list', 601 '_do_execute', 602 '_execute_str') 603 604 def __init__(self, *args, **kw): 605 if SCons.Debug.track_instances: 606 logInstanceCreation(self, 'Executor.Null') 607 self.batches = [Batch(kw['targets'][:], [])] 608 def get_build_env(self): 609 return get_NullEnvironment() 610 def get_build_scanner_path(self): 611 return None 612 def cleanup(self): 613 pass 614 def prepare(self): 615 pass 616 def get_unignored_sources(self, *args, **kw): 617 return tuple(()) 618 def get_action_targets(self): 619 return [] 620 def get_action_list(self): 621 return [] 622 def get_all_targets(self): 623 return self.batches[0].targets 624 def get_all_sources(self): 625 return self.batches[0].targets[0].sources 626 def get_all_children(self): 627 return self.batches[0].targets[0].children() 628 def get_all_prerequisites(self): 629 return [] 630 def get_action_side_effects(self): 631 return [] 632 def __call__(self, *args, **kw): 633 return 0 634 def get_contents(self): 635 return '' 636 def _morph(self): 637 """Morph this Null executor to a real Executor object.""" 638 batches = self.batches 639 self.__class__ = Executor 640 self.__init__([]) 641 self.batches = batches 642 643 # The following methods require morphing this Null Executor to a 644 # real Executor object. 645 646 def add_pre_action(self, action): 647 self._morph() 648 self.add_pre_action(action) 649 def add_post_action(self, action): 650 self._morph() 651 self.add_post_action(action) 652 def set_action_list(self, action): 653 self._morph() 654 self.set_action_list(action) 655 656# Local Variables: 657# tab-width:4 658# indent-tabs-mode:nil 659# End: 660# vim: set expandtab tabstop=4 shiftwidth=4: 661