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