summaryrefslogtreecommitdiffstats
path: root/src/engine/SCons/Script/Interactive.py
blob: 206f94101d68d123b2e209894ad427dc7aded674 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#
# __COPYRIGHT__
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"

__doc__ = """
SCons interactive mode
"""

# TODO:
#
# This has the potential to grow into something with a really big life
# of its own, which might or might not be a good thing.  Nevertheless,
# here are some enhancements that will probably be requested some day
# and are worth keeping in mind (assuming this takes off):
# 
# - A command to re-read / re-load the SConscript files.  This may
#   involve allowing people to specify command-line options (e.g. -f,
#   -I, --no-site-dir) that affect how the SConscript files are read.
#
# - Additional command-line options on the "build" command.
#
#   Of the supported options that seemed to make sense (after a quick
#   pass through the list), the ones that seemed likely enough to be
#   used are listed in the man page and have explicit test scripts.
#
#   These had code changed in Script/Main.py to support them, but didn't
#   seem likely to be used regularly, so had no test scripts added:
#
#       build --diskcheck=*
#       build --implicit-cache=*
#       build --implicit-deps-changed=*
#       build --implicit-deps-unchanged=*
#
#   These look like they should "just work" with no changes to the
#   existing code, but like those above, look unlikely to be used and
#   therefore had no test scripts added:
#
#       build --random
#
#   These I'm not sure about.  They might be useful for individual
#   "build" commands, and may even work, but they seem unlikely enough
#   that we'll wait until they're requested before spending any time on
#   writing test scripts for them, or investigating whether they work.
#
#       build -q [???  is there a useful analog to the exit status?]
#       build --duplicate=
#       build --profile=
#       build --max-drift=
#       build --warn=*
#       build --Y
#
# - Most of the SCons command-line options that the "build" command
#   supports should be settable as default options that apply to all
#   subsequent "build" commands.  Maybe a "set {option}" command that
#   maps to "SetOption('{option}')".
#
# - Need something in the 'help' command that prints the -h output.
#
# - A command to run the configure subsystem separately (must see how
#   this interacts with the new automake model).
#
# - Command-line completion of target names; maybe even of SCons options?
#   Completion is something that's supported by the Python cmd module,
#   so this should be doable without too much trouble.
#

import cmd
import copy
import os
import re
import shlex
import string
import sys

try:
    import readline
except ImportError:
    pass

class SConsInteractiveCmd(cmd.Cmd):
    """\
    build [TARGETS]         Build the specified TARGETS and their dependencies.
                            'b' is a synonym.
    clean [TARGETS]         Clean (remove) the specified TARGETS and their
                            dependencies.  'c' is a synonym.
    exit                    Exit SCons interactive mode.
    help [COMMAND]          Prints help for the specified COMMAND.  'h' and
                            '?' are synonyms.
    shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and '!'
                            are synonyms.
    version                 Prints SCons version information.
    """

    synonyms = {
        'b'     : 'build',
        'c'     : 'clean',
        'h'     : 'help',
        'scons' : 'build',
        'sh'    : 'shell',
    }

    def __init__(self, **kw):
        cmd.Cmd.__init__(self)
        for key, val in kw.items():
            setattr(self, key, val)

        if sys.platform == 'win32':
            self.shell_variable = 'COMSPEC'
        else:
            self.shell_variable = 'SHELL'

    def default(self, argv):
        print "*** Unknown command: %s" % argv[0]

    def onecmd(self, line):
        line = string.strip(line)
        if not line:
            print self.lastcmd
            return self.emptyline()
        self.lastcmd = line
        if line[0] == '!':
            line = 'shell ' + line[1:]
        elif line[0] == '?':
            line = 'help ' + line[1:]
        if os.sep == '\\':
            line = string.replace(line, '\\', '\\\\')
        argv = shlex.split(line)
        argv[0] = self.synonyms.get(argv[0], argv[0])
        if not argv[0]:
            return self.default(line)
        else:
            try:
                func = getattr(self, 'do_' + argv[0])
            except AttributeError:
                return self.default(argv)
            return func(argv)

    def do_build(self, argv):
        """\
        build [TARGETS]         Build the specified TARGETS and their
                                dependencies.  'b' is a synonym.
        """
        import SCons.Node
        import SCons.SConsign
        import SCons.Script.Main

        options = copy.deepcopy(self.options)

        options, targets = self.parser.parse_args(argv[1:], values=options)

        SCons.Script.COMMAND_LINE_TARGETS = targets

        if targets:
            SCons.Script.BUILD_TARGETS = targets
        else:
            # If the user didn't specify any targets on the command line,
            # use the list of default targets.
            SCons.Script.BUILD_TARGETS = SCons.Script._build_plus_default

        nodes = SCons.Script.Main._build_targets(self.fs,
                                                 options,
                                                 targets,
                                                 self.target_top)

        if not nodes:
            return

        # Call each of the Node's alter_targets() methods, which may
        # provide additional targets that ended up as part of the build
        # (the canonical example being a VariantDir() when we're building
        # from a source directory) and which we therefore need their
        # state cleared, too.
        x = []
        for n in nodes:
            x.extend(n.alter_targets()[0])
        nodes.extend(x)

        # Clean up so that we can perform the next build correctly.
        #
        # We do this by walking over all the children of the targets,
        # and clearing their state.
        #
        # We currently have to re-scan each node to find their
        # children, because built nodes have already been partially
        # cleared and don't remember their children.  (In scons
        # 0.96.1 and earlier, this wasn't the case, and we didn't
        # have to re-scan the nodes.)
        #
        # Because we have to re-scan each node, we can't clear the
        # nodes as we walk over them, because we may end up rescanning
        # a cleared node as we scan a later node.  Therefore, only
        # store the list of nodes that need to be cleared as we walk
        # the tree, and clear them in a separate pass.
        #
        # XXX: Someone more familiar with the inner workings of scons
        # may be able to point out a more efficient way to do this.

        SCons.Script.Main.progress_display("scons: Clearing cached node information ...")

        seen_nodes = {}

        def get_unseen_children(node, parent, seen_nodes=seen_nodes):
            def is_unseen(node, seen_nodes=seen_nodes):
                return not seen_nodes.has_key(node)
            return filter(is_unseen, node.children(scan=1))

        def add_to_seen_nodes(node, parent, seen_nodes=seen_nodes):
            seen_nodes[node] = 1

            # If this file is in a VariantDir and has a
            # corresponding source file in the source tree, remember the
            # node in the source tree, too.  This is needed in
            # particular to clear cached implicit dependencies on the
            # source file, since the scanner will scan it if the
            # VariantDir was created with duplicate=0.
            try:
                rfile_method = node.rfile
            except AttributeError:
                return
            else:
                rfile = rfile_method()
            if rfile != node:
                seen_nodes[rfile] = 1

        for node in nodes:
            walker = SCons.Node.Walker(node,
                                        kids_func=get_unseen_children,
                                        eval_func=add_to_seen_nodes)
            n = walker.next()
            while n:
                n = walker.next()

        for node in seen_nodes.keys():
            # Call node.clear() to clear most of the state
            node.clear()
            # node.clear() doesn't reset node.state, so call
            # node.set_state() to reset it manually
            node.set_state(SCons.Node.no_state)
            node.implicit = None

            # Debug:  Uncomment to verify that all Taskmaster reference
            # counts have been reset to zero.
            #if node.ref_count != 0:
            #    from SCons.Debug import Trace
            #    Trace('node %s, ref_count %s !!!\n' % (node, node.ref_count))

        SCons.SConsign.Reset()
        SCons.Script.Main.progress_display("scons: done clearing node information.")

    def do_clean(self, argv):
        """\
        clean [TARGETS]         Clean (remove) the specified TARGETS
                                and their dependencies.  'c' is a synonym.
        """
        return self.do_build(['build', '--clean'] + argv[1:])

    def do_EOF(self, argv):
        print
        self.do_exit(argv)

    def _do_one_help(self, arg):
        try:
            # If help_<arg>() exists, then call it.
            func = getattr(self, 'help_' + arg)
        except AttributeError:
            try:
                func = getattr(self, 'do_' + arg)
            except AttributeError:
                doc = None
            else:
                doc = self._doc_to_help(func)
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()
        else:
            doc = self.strip_initial_spaces(func())
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()

    def _doc_to_help(self, obj):
        doc = obj.__doc__
        if doc is None:
            return ''
        return self._strip_initial_spaces(doc)

    def _strip_initial_spaces(self, s):
        #lines = s.split('\n')
        lines = string.split(s, '\n')
        spaces = re.match(' *', lines[0]).group(0)
        #def strip_spaces(l):
        #    if l.startswith(spaces):
        #        l = l[len(spaces):]
        #    return l
        #return '\n'.join([ strip_spaces(l) for l in lines ])
        def strip_spaces(l, spaces=spaces):
            if l[:len(spaces)] == spaces:
                l = l[len(spaces):]
            return l
        lines = map(strip_spaces, lines)
        return string.join(lines, '\n')

    def do_exit(self, argv):
        """\
        exit                    Exit SCons interactive mode.
        """
        sys.exit(0)

    def do_help(self, argv):
        """\
        help [COMMAND]          Prints help for the specified COMMAND.  'h'
                                and '?' are synonyms.
        """
        if argv[1:]:
            for arg in argv[1:]:
                if self._do_one_help(arg):
                    break
        else:
            # If bare 'help' is called, print this class's doc
            # string (if it has one).
            doc = self._doc_to_help(self.__class__)
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()

    def do_shell(self, argv):
        """\
        shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and
                                '!' are synonyms.
        """
        import subprocess
        argv = argv[1:]
        if not argv:
            argv = os.environ[self.shell_variable]
        try:
            # Per "[Python-Dev] subprocess insufficiently platform-independent?"
            # http://mail.python.org/pipermail/python-dev/2008-August/081979.html "+
            # Doing the right thing with an argument list currently
            # requires different shell= values on Windows and Linux.
            p = subprocess.Popen(argv, shell=(sys.platform=='win32'))
        except EnvironmentError, e:
            sys.stderr.write('scons: %s: %s\n' % (argv[0], e.strerror))
        else:
            p.wait()

    def do_version(self, argv):
        """\
        version                 Prints SCons version information.
        """
        sys.stdout.write(self.parser.version + '\n')

def interact(fs, parser, options, targets, target_top):
    c = SConsInteractiveCmd(prompt = 'scons>>> ',
                            fs = fs,
                            parser = parser,
                            options = options,
                            targets = targets,
                            target_top = target_top)
    c.cmdloop()

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4: