1#
2# Copyright (c) 2001 - 2014 The SCons Foundation
3#
4# Permission is hereby granted, free of charge, to any person obtaining
5# a copy of this software and associated documentation files (the
6# "Software"), to deal in the Software without restriction, including
7# without limitation the rights to use, copy, modify, merge, publish,
8# distribute, sublicense, and/or sell copies of the Software, and to
9# permit persons to whom the Software is furnished to do so, subject to
10# the following conditions:
11#
12# The above copyright notice and this permission notice shall be included
13# in all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
16# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
17# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
23__revision__ = "src/engine/SCons/Script/Interactive.py  2014/07/05 09:42:21 garyo"
24
25__doc__ = """
26SCons interactive mode
27"""
28
29# TODO:
30#
31# This has the potential to grow into something with a really big life
32# of its own, which might or might not be a good thing.  Nevertheless,
33# here are some enhancements that will probably be requested some day
34# and are worth keeping in mind (assuming this takes off):
35#
36# - A command to re-read / re-load the SConscript files.  This may
37#   involve allowing people to specify command-line options (e.g. -f,
38#   -I, --no-site-dir) that affect how the SConscript files are read.
39#
40# - Additional command-line options on the "build" command.
41#
42#   Of the supported options that seemed to make sense (after a quick
43#   pass through the list), the ones that seemed likely enough to be
44#   used are listed in the man page and have explicit test scripts.
45#
46#   These had code changed in Script/Main.py to support them, but didn't
47#   seem likely to be used regularly, so had no test scripts added:
48#
49#       build --diskcheck=*
50#       build --implicit-cache=*
51#       build --implicit-deps-changed=*
52#       build --implicit-deps-unchanged=*
53#
54#   These look like they should "just work" with no changes to the
55#   existing code, but like those above, look unlikely to be used and
56#   therefore had no test scripts added:
57#
58#       build --random
59#
60#   These I'm not sure about.  They might be useful for individual
61#   "build" commands, and may even work, but they seem unlikely enough
62#   that we'll wait until they're requested before spending any time on
63#   writing test scripts for them, or investigating whether they work.
64#
65#       build -q [???  is there a useful analog to the exit status?]
66#       build --duplicate=
67#       build --profile=
68#       build --max-drift=
69#       build --warn=*
70#       build --Y
71#
72# - Most of the SCons command-line options that the "build" command
73#   supports should be settable as default options that apply to all
74#   subsequent "build" commands.  Maybe a "set {option}" command that
75#   maps to "SetOption('{option}')".
76#
77# - Need something in the 'help' command that prints the -h output.
78#
79# - A command to run the configure subsystem separately (must see how
80#   this interacts with the new automake model).
81#
82# - Command-line completion of target names; maybe even of SCons options?
83#   Completion is something that's supported by the Python cmd module,
84#   so this should be doable without too much trouble.
85#
86
87import cmd
88import copy
89import os
90import re
91import shlex
92import sys
93
94try:
95    import readline
96except ImportError:
97    pass
98
99class SConsInteractiveCmd(cmd.Cmd):
100    """\
101    build [TARGETS]         Build the specified TARGETS and their dependencies.
102                            'b' is a synonym.
103    clean [TARGETS]         Clean (remove) the specified TARGETS and their
104                            dependencies.  'c' is a synonym.
105    exit                    Exit SCons interactive mode.
106    help [COMMAND]          Prints help for the specified COMMAND.  'h' and
107                            '?' are synonyms.
108    shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and '!'
109                            are synonyms.
110    version                 Prints SCons version information.
111    """
112
113    synonyms = {
114        'b'     : 'build',
115        'c'     : 'clean',
116        'h'     : 'help',
117        'scons' : 'build',
118        'sh'    : 'shell',
119    }
120
121    def __init__(self, **kw):
122        cmd.Cmd.__init__(self)
123        for key, val in kw.items():
124            setattr(self, key, val)
125
126        if sys.platform == 'win32':
127            self.shell_variable = 'COMSPEC'
128        else:
129            self.shell_variable = 'SHELL'
130
131    def default(self, argv):
132        print "*** Unknown command: %s" % argv[0]
133
134    def onecmd(self, line):
135        line = line.strip()
136        if not line:
137            print self.lastcmd
138            return self.emptyline()
139        self.lastcmd = line
140        if line[0] == '!':
141            line = 'shell ' + line[1:]
142        elif line[0] == '?':
143            line = 'help ' + line[1:]
144        if os.sep == '\\':
145            line = line.replace('\\', '\\\\')
146        argv = shlex.split(line)
147        argv[0] = self.synonyms.get(argv[0], argv[0])
148        if not argv[0]:
149            return self.default(line)
150        else:
151            try:
152                func = getattr(self, 'do_' + argv[0])
153            except AttributeError:
154                return self.default(argv)
155            return func(argv)
156
157    def do_build(self, argv):
158        """\
159        build [TARGETS]         Build the specified TARGETS and their
160                                dependencies.  'b' is a synonym.
161        """
162        import SCons.Node
163        import SCons.SConsign
164        import SCons.Script.Main
165
166        options = copy.deepcopy(self.options)
167
168        options, targets = self.parser.parse_args(argv[1:], values=options)
169
170        SCons.Script.COMMAND_LINE_TARGETS = targets
171
172        if targets:
173            SCons.Script.BUILD_TARGETS = targets
174        else:
175            # If the user didn't specify any targets on the command line,
176            # use the list of default targets.
177            SCons.Script.BUILD_TARGETS = SCons.Script._build_plus_default
178
179        nodes = SCons.Script.Main._build_targets(self.fs,
180                                                 options,
181                                                 targets,
182                                                 self.target_top)
183
184        if not nodes:
185            return
186
187        # Call each of the Node's alter_targets() methods, which may
188        # provide additional targets that ended up as part of the build
189        # (the canonical example being a VariantDir() when we're building
190        # from a source directory) and which we therefore need their
191        # state cleared, too.
192        x = []
193        for n in nodes:
194            x.extend(n.alter_targets()[0])
195        nodes.extend(x)
196
197        # Clean up so that we can perform the next build correctly.
198        #
199        # We do this by walking over all the children of the targets,
200        # and clearing their state.
201        #
202        # We currently have to re-scan each node to find their
203        # children, because built nodes have already been partially
204        # cleared and don't remember their children.  (In scons
205        # 0.96.1 and earlier, this wasn't the case, and we didn't
206        # have to re-scan the nodes.)
207        #
208        # Because we have to re-scan each node, we can't clear the
209        # nodes as we walk over them, because we may end up rescanning
210        # a cleared node as we scan a later node.  Therefore, only
211        # store the list of nodes that need to be cleared as we walk
212        # the tree, and clear them in a separate pass.
213        #
214        # XXX: Someone more familiar with the inner workings of scons
215        # may be able to point out a more efficient way to do this.
216
217        SCons.Script.Main.progress_display("scons: Clearing cached node information ...")
218
219        seen_nodes = {}
220
221        def get_unseen_children(node, parent, seen_nodes=seen_nodes):
222            def is_unseen(node, seen_nodes=seen_nodes):
223                return node not in seen_nodes
224            return list(filter(is_unseen, node.children(scan=1)))
225
226        def add_to_seen_nodes(node, parent, seen_nodes=seen_nodes):
227            seen_nodes[node] = 1
228
229            # If this file is in a VariantDir and has a
230            # corresponding source file in the source tree, remember the
231            # node in the source tree, too.  This is needed in
232            # particular to clear cached implicit dependencies on the
233            # source file, since the scanner will scan it if the
234            # VariantDir was created with duplicate=0.
235            try:
236                rfile_method = node.rfile
237            except AttributeError:
238                return
239            else:
240                rfile = rfile_method()
241            if rfile != node:
242                seen_nodes[rfile] = 1
243
244        for node in nodes:
245            walker = SCons.Node.Walker(node,
246                                        kids_func=get_unseen_children,
247                                        eval_func=add_to_seen_nodes)
248            n = walker.get_next()
249            while n:
250                n = walker.get_next()
251
252        for node in seen_nodes.keys():
253            # Call node.clear() to clear most of the state
254            node.clear()
255            # node.clear() doesn't reset node.state, so call
256            # node.set_state() to reset it manually
257            node.set_state(SCons.Node.no_state)
258            node.implicit = None
259
260            # Debug:  Uncomment to verify that all Taskmaster reference
261            # counts have been reset to zero.
262            #if node.ref_count != 0:
263            #    from SCons.Debug import Trace
264            #    Trace('node %s, ref_count %s !!!\n' % (node, node.ref_count))
265
266        SCons.SConsign.Reset()
267        SCons.Script.Main.progress_display("scons: done clearing node information.")
268
269    def do_clean(self, argv):
270        """\
271        clean [TARGETS]         Clean (remove) the specified TARGETS
272                                and their dependencies.  'c' is a synonym.
273        """
274        return self.do_build(['build', '--clean'] + argv[1:])
275
276    def do_EOF(self, argv):
277        print
278        self.do_exit(argv)
279
280    def _do_one_help(self, arg):
281        try:
282            # If help_<arg>() exists, then call it.
283            func = getattr(self, 'help_' + arg)
284        except AttributeError:
285            try:
286                func = getattr(self, 'do_' + arg)
287            except AttributeError:
288                doc = None
289            else:
290                doc = self._doc_to_help(func)
291            if doc:
292                sys.stdout.write(doc + '\n')
293                sys.stdout.flush()
294        else:
295            doc = self.strip_initial_spaces(func())
296            if doc:
297                sys.stdout.write(doc + '\n')
298                sys.stdout.flush()
299
300    def _doc_to_help(self, obj):
301        doc = obj.__doc__
302        if doc is None:
303            return ''
304        return self._strip_initial_spaces(doc)
305
306    def _strip_initial_spaces(self, s):
307        #lines = s.split('\n')
308        lines = s.split('\n')
309        spaces = re.match(' *', lines[0]).group(0)
310        #def strip_spaces(l):
311        #    if l.startswith(spaces):
312        #        l = l[len(spaces):]
313        #    return l
314        #return '\n'.join([ strip_spaces(l) for l in lines ])
315        def strip_spaces(l, spaces=spaces):
316            if l[:len(spaces)] == spaces:
317                l = l[len(spaces):]
318            return l
319        lines = list(map(strip_spaces, lines))
320        return '\n'.join(lines)
321
322    def do_exit(self, argv):
323        """\
324        exit                    Exit SCons interactive mode.
325        """
326        sys.exit(0)
327
328    def do_help(self, argv):
329        """\
330        help [COMMAND]          Prints help for the specified COMMAND.  'h'
331                                and '?' are synonyms.
332        """
333        if argv[1:]:
334            for arg in argv[1:]:
335                if self._do_one_help(arg):
336                    break
337        else:
338            # If bare 'help' is called, print this class's doc
339            # string (if it has one).
340            doc = self._doc_to_help(self.__class__)
341            if doc:
342                sys.stdout.write(doc + '\n')
343                sys.stdout.flush()
344
345    def do_shell(self, argv):
346        """\
347        shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and
348                                '!' are synonyms.
349        """
350        import subprocess
351        argv = argv[1:]
352        if not argv:
353            argv = os.environ[self.shell_variable]
354        try:
355            # Per "[Python-Dev] subprocess insufficiently platform-independent?"
356            # http://mail.python.org/pipermail/python-dev/2008-August/081979.html "+
357            # Doing the right thing with an argument list currently
358            # requires different shell= values on Windows and Linux.
359            p = subprocess.Popen(argv, shell=(sys.platform=='win32'))
360        except EnvironmentError, e:
361            sys.stderr.write('scons: %s: %s\n' % (argv[0], e.strerror))
362        else:
363            p.wait()
364
365    def do_version(self, argv):
366        """\
367        version                 Prints SCons version information.
368        """
369        sys.stdout.write(self.parser.version + '\n')
370
371def interact(fs, parser, options, targets, target_top):
372    c = SConsInteractiveCmd(prompt = 'scons>>> ',
373                            fs = fs,
374                            parser = parser,
375                            options = options,
376                            targets = targets,
377                            target_top = target_top)
378    c.cmdloop()
379
380# Local Variables:
381# tab-width:4
382# indent-tabs-mode:nil
383# End:
384# vim: set expandtab tabstop=4 shiftwidth=4:
385