# MIT License # # Copyright The SCons Foundation # # 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. """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 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) -> None: 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) -> None: print("*** Unknown command: %s" % argv[0]) def onecmd(self, line): line = line.strip() 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 = line.replace('\\', '\\\\') 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) -> None: """\ 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) -> bool: return node not in seen_nodes return [child for child in node.children(scan=1) if is_unseen(child)] def add_to_seen_nodes(node, parent, seen_nodes=seen_nodes) -> None: 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.get_next() while n: n = walker.get_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)) # TODO: REMOVE WPD DEBUG 02/14/2022 # This call was clearing the list of sconsign files to be written, so it would # only write the results of the first build command. All others wouldn't be written # to .SConsign. # Pretty sure commenting this out is the correct fix. # 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) -> None: print() self.do_exit(argv) def _do_one_help(self, arg) -> None: try: # If help_() 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') spaces = re.match(' *', lines[0]).group(0) def strip_spaces(l, spaces=spaces): if l[:len(spaces)] == spaces: l = l[len(spaces):] return l lines = list(map(strip_spaces, lines)) return '\n'.join(lines) def do_exit(self, argv) -> None: """\ exit Exit SCons interactive mode. """ sys.exit(0) def do_help(self, argv) -> None: """\ 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) -> None: """\ 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?" # https://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 OSError as e: sys.stderr.write('scons: %s: %s\n' % (argv[0], e.strerror)) else: p.wait() def do_version(self, argv) -> None: """\ version Prints SCons version information. """ sys.stdout.write(self.parser.version + '\n') def interact(fs, parser, options, targets, target_top) -> None: 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: