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