updated copyright on README
[scons.git] / SCons / Script / Main.py
blob77d80cb293aebfed029f62174c53442db834538e
1 # MIT License
3 # Copyright The SCons Foundation
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:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
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.
24 """The main() function used by the scons script.
26 Architecturally, this *is* the scons script, and will likely only be
27 called from the external "scons" wrapper. Consequently, anything here
28 should not be, or be considered, part of the build engine. If it's
29 something that we expect other software to want to use, it should go in
30 some other module. If it's specific to the "scons" script invocation,
31 it goes here.
32 """
34 import SCons.compat
36 import importlib.util
37 import os
38 import re
39 import sys
40 import time
41 import traceback
42 import platform
43 import threading
44 from typing import Optional, List, TYPE_CHECKING
46 import SCons.CacheDir
47 import SCons.Debug
48 import SCons.Defaults
49 import SCons.Environment
50 import SCons.Errors
51 import SCons.Taskmaster.Job
52 import SCons.Node
53 import SCons.Node.FS
54 import SCons.Platform
55 import SCons.Platform.virtualenv
56 import SCons.SConf
57 import SCons.Script
58 import SCons.Taskmaster
59 import SCons.Util
60 import SCons.Warnings
61 import SCons.Script.Interactive
62 if TYPE_CHECKING:
63 from SCons.Script import SConsOption
64 from SCons.Util.stats import count_stats, memory_stats, time_stats, ENABLE_JSON, write_scons_stats_file, JSON_OUTPUT_FILE
66 from SCons import __version__ as SConsVersion
68 # these define the range of versions SCons supports
69 minimum_python_version = (3, 6, 0)
70 deprecated_python_version = (3, 7, 0)
72 # ordered list of SConstruct names to look for if there is no -f flag
73 KNOWN_SCONSTRUCT_NAMES = [
74 'SConstruct',
75 'Sconstruct',
76 'sconstruct',
77 'SConstruct.py',
78 'Sconstruct.py',
79 'sconstruct.py',
82 # list of names recognized by debugger as "SConscript files" (inc. SConstruct)
83 # files suffixed .py always work so don't need to be in this list.
84 KNOWN_SCONSCRIPTS = [
85 "SConstruct",
86 "Sconstruct",
87 "sconstruct",
88 "SConscript",
89 "Sconscript",
90 "sconscript",
91 "SCsub", # Uncommon alternative to SConscript
92 "Scsub",
93 "scsub",
96 # Global variables
97 first_command_start = None
98 last_command_end = None
99 print_objects = False
100 print_memoizer = False
101 print_stacktrace = False
102 print_time = False
103 print_action_timestamps = False
104 sconscript_time = 0
105 cumulative_command_time = 0
106 exit_status = 0 # final exit status, assume success by default
107 this_build_status = 0 # "exit status" of an individual build
108 num_jobs = None
109 delayed_warnings = []
112 def revert_io() -> None:
113 # This call is added to revert stderr and stdout to the original
114 # ones just in case some build rule or something else in the system
115 # has redirected them elsewhere.
116 sys.stderr = sys.__stderr__
117 sys.stdout = sys.__stdout__
120 class SConsPrintHelpException(Exception):
121 pass
124 display = SCons.Util.display
125 progress_display = SCons.Util.DisplayEngine()
128 class Progressor:
129 prev = ''
130 count = 0
131 target_string = '$TARGET'
133 def __init__(self, obj, interval: int=1, file=None, overwrite: bool=False) -> None:
134 if file is None:
135 file = sys.stdout
137 self.obj = obj
138 self.file = file
139 self.interval = interval
140 self.overwrite = overwrite
142 if callable(obj):
143 self.func = obj
144 elif SCons.Util.is_List(obj):
145 self.func = self.spinner
146 elif obj.find(self.target_string) != -1:
147 self.func = self.replace_string
148 else:
149 self.func = self.string
151 def write(self, s) -> None:
152 self.file.write(s)
153 self.file.flush()
154 self.prev = s
156 def erase_previous(self) -> None:
157 if self.prev:
158 length = len(self.prev)
159 if self.prev[-1] in ('\n', '\r'):
160 length = length - 1
161 self.write(' ' * length + '\r')
162 self.prev = ''
164 def spinner(self, node) -> None:
165 self.write(self.obj[self.count % len(self.obj)])
167 def string(self, node) -> None:
168 self.write(self.obj)
170 def replace_string(self, node) -> None:
171 self.write(self.obj.replace(self.target_string, str(node)))
173 def __call__(self, node) -> None:
174 self.count = self.count + 1
175 if (self.count % self.interval) == 0:
176 if self.overwrite:
177 self.erase_previous()
178 self.func(node)
180 ProgressObject = SCons.Util.Null()
182 def Progress(*args, **kw) -> None:
183 """Show progress during building - Public API."""
184 global ProgressObject
185 ProgressObject = Progressor(*args, **kw)
187 # Task control.
190 _BuildFailures = []
193 def GetBuildFailures():
194 return _BuildFailures
197 class BuildTask(SCons.Taskmaster.OutOfDateTask):
198 """An SCons build task."""
199 progress = ProgressObject
201 def display(self, message) -> None:
202 display('scons: ' + message)
204 def prepare(self):
205 if not isinstance(self.progress, SCons.Util.Null):
206 for target in self.targets:
207 self.progress(target)
208 return SCons.Taskmaster.OutOfDateTask.prepare(self)
210 def needs_execute(self) -> bool:
211 if SCons.Taskmaster.OutOfDateTask.needs_execute(self):
212 return True
213 if self.top and self.targets[0].has_builder():
214 display("scons: `%s' is up to date." % str(self.node))
215 return False
217 def execute(self) -> None:
218 if print_time:
219 start_time = time.time()
220 global first_command_start
221 if first_command_start is None:
222 first_command_start = start_time
223 SCons.Taskmaster.OutOfDateTask.execute(self)
224 if print_time:
225 global cumulative_command_time
226 global last_command_end
227 finish_time = time.time()
228 last_command_end = finish_time
229 cumulative_command_time += finish_time - start_time
230 if print_action_timestamps:
231 sys.stdout.write(
232 "Command execution start timestamp: %s: %f\n"
233 % (str(self.node), start_time)
235 sys.stdout.write(
236 "Command execution end timestamp: %s: %f\n"
237 % (str(self.node), finish_time)
239 time_stats.add_command(str(self.node), start_time, finish_time)
240 sys.stdout.write(
241 "Command execution time: %s: %f seconds\n"
242 % (str(self.node), (finish_time - start_time))
245 def do_failed(self, status: int=2) -> None:
246 _BuildFailures.append(self.exception[1])
247 global exit_status
248 global this_build_status
249 if self.options.ignore_errors:
250 SCons.Taskmaster.OutOfDateTask.executed(self)
251 elif self.options.keep_going:
252 SCons.Taskmaster.OutOfDateTask.fail_continue(self)
253 exit_status = status
254 this_build_status = status
255 else:
256 SCons.Taskmaster.OutOfDateTask.fail_stop(self)
257 exit_status = status
258 this_build_status = status
260 def executed(self):
261 t = self.targets[0]
262 if self.top and not t.has_builder() and not t.side_effect:
263 if not t.exists():
264 if t.__class__.__name__ in ('File', 'Dir', 'Entry'):
265 errstr="Do not know how to make %s target `%s' (%s)." % (t.__class__.__name__, t, t.get_abspath())
266 else: # Alias or Python or ...
267 errstr="Do not know how to make %s target `%s'." % (t.__class__.__name__, t)
268 sys.stderr.write("scons: *** " + errstr)
269 if not self.options.keep_going:
270 sys.stderr.write(" Stop.")
271 sys.stderr.write("\n")
272 try:
273 raise SCons.Errors.BuildError(t, errstr)
274 except KeyboardInterrupt:
275 raise
276 except:
277 self.exception_set()
278 self.do_failed()
279 else:
280 print("scons: Nothing to be done for `%s'." % t)
281 SCons.Taskmaster.OutOfDateTask.executed(self)
282 else:
283 SCons.Taskmaster.OutOfDateTask.executed(self)
285 def failed(self) -> None:
286 # Handle the failure of a build task. The primary purpose here
287 # is to display the various types of Errors and Exceptions
288 # appropriately.
289 exc_info = self.exc_info()
290 try:
291 t, e, tb = exc_info
292 except ValueError:
293 t, e = exc_info
294 tb = None
296 if t is None:
297 # The Taskmaster didn't record an exception for this Task;
298 # see if the sys module has one.
299 try:
300 t, e, tb = sys.exc_info()[:]
301 except ValueError:
302 t, e = exc_info
303 tb = None
305 # Deprecated string exceptions will have their string stored
306 # in the first entry of the tuple.
307 if e is None:
308 e = t
310 buildError = SCons.Errors.convert_to_BuildError(e)
311 if not buildError.node:
312 buildError.node = self.node
314 node = buildError.node
315 if not SCons.Util.is_List(node):
316 node = [node]
317 nodename = ', '.join(map(str, node))
319 errfmt = "scons: *** [%s] %s\n"
320 sys.stderr.write(errfmt % (nodename, buildError))
322 if (buildError.exc_info[2] and buildError.exc_info[1] and
323 not isinstance(
324 buildError.exc_info[1],
325 (EnvironmentError, SCons.Errors.StopError,
326 SCons.Errors.UserError))):
327 type, value, trace = buildError.exc_info
328 if tb and print_stacktrace:
329 sys.stderr.write("scons: internal stack trace:\n")
330 traceback.print_tb(tb, file=sys.stderr)
331 traceback.print_exception(type, value, trace)
332 elif tb and print_stacktrace:
333 sys.stderr.write("scons: internal stack trace:\n")
334 traceback.print_tb(tb, file=sys.stderr)
336 self.exception = (e, buildError, tb) # type, value, traceback
337 self.do_failed(buildError.exitstatus)
339 self.exc_clear()
341 def postprocess(self) -> None:
342 if self.top:
343 t = self.targets[0]
344 for tp in self.options.tree_printers:
345 tp.display(t)
346 if self.options.debug_includes:
347 tree = t.render_include_tree()
348 if tree:
349 print()
350 print(tree)
351 SCons.Taskmaster.OutOfDateTask.postprocess(self)
353 def make_ready(self) -> None:
354 """Make a task ready for execution"""
355 SCons.Taskmaster.OutOfDateTask.make_ready(self)
356 if self.out_of_date and self.options.debug_explain:
357 explanation = self.out_of_date[0].explain()
358 if explanation:
359 sys.stdout.write("scons: " + explanation)
362 class CleanTask(SCons.Taskmaster.AlwaysTask):
363 """An SCons clean task."""
364 def fs_delete(self, path, pathstr, remove: bool=True):
365 try:
366 if os.path.lexists(path):
367 if os.path.isfile(path) or os.path.islink(path):
368 if remove: os.unlink(path)
369 display("Removed " + pathstr)
370 elif os.path.isdir(path) and not os.path.islink(path):
371 # delete everything in the dir
372 for e in sorted(os.listdir(path)):
373 p = os.path.join(path, e)
374 s = os.path.join(pathstr, e)
375 if os.path.isfile(p):
376 if remove: os.unlink(p)
377 display("Removed " + s)
378 else:
379 self.fs_delete(p, s, remove)
380 # then delete dir itself
381 if remove: os.rmdir(path)
382 display("Removed directory " + pathstr)
383 else:
384 errstr = "Path '%s' exists but isn't a file or directory."
385 raise SCons.Errors.UserError(errstr % pathstr)
386 except SCons.Errors.UserError as e:
387 print(e)
388 except OSError as e:
389 print("scons: Could not remove '%s':" % pathstr, e.strerror)
391 def _get_files_to_clean(self):
392 result = []
393 target = self.targets[0]
394 if target.has_builder() or target.side_effect:
395 result = [t for t in self.targets if not t.noclean]
396 return result
398 def _clean_targets(self, remove: bool=True) -> None:
399 target = self.targets[0]
400 if target in SCons.Environment.CleanTargets:
401 files = SCons.Environment.CleanTargets[target]
402 for f in files:
403 self.fs_delete(f.get_abspath(), str(f), remove)
405 def show(self) -> None:
406 for t in self._get_files_to_clean():
407 if not t.isdir():
408 display("Removed " + str(t))
409 self._clean_targets(remove=False)
411 def remove(self) -> None:
412 for t in self._get_files_to_clean():
413 try:
414 removed = t.remove()
415 except OSError as e:
416 # An OSError may indicate something like a permissions
417 # issue, an IOError would indicate something like
418 # the file not existing. In either case, print a
419 # message and keep going to try to remove as many
420 # targets as possible.
421 print(f"scons: Could not remove '{str(t)}'", e.strerror)
422 else:
423 if removed:
424 display("Removed " + str(t))
425 self._clean_targets(remove=True)
427 execute = remove
429 # We want the Taskmaster to update the Node states (and therefore
430 # handle reference counts, etc.), but we don't want to call
431 # back to the Node's post-build methods, which would do things
432 # we don't want, like store .sconsign information.
433 executed = SCons.Taskmaster.Task.executed_without_callbacks
435 # Have the Taskmaster arrange to "execute" all of the targets, because
436 # we'll figure out ourselves (in remove() or show() above) whether
437 # anything really needs to be done.
438 make_ready = SCons.Taskmaster.Task.make_ready_all
440 def prepare(self) -> None:
441 pass
443 class QuestionTask(SCons.Taskmaster.AlwaysTask):
444 """An SCons task for the -q (question) option."""
445 def prepare(self) -> None:
446 pass
448 def execute(self) -> None:
449 if self.targets[0].get_state() != SCons.Node.up_to_date or \
450 (self.top and not self.targets[0].exists()):
451 global exit_status
452 global this_build_status
453 exit_status = 1
454 this_build_status = 1
455 self.tm.stop()
457 def executed(self) -> None:
458 pass
461 class TreePrinter:
462 def __init__(self, derived: bool=False, prune: bool=False, status: bool=False, sLineDraw: bool=False) -> None:
463 self.derived = derived
464 self.prune = prune
465 self.status = status
466 self.sLineDraw = sLineDraw
467 def get_all_children(self, node):
468 return node.all_children()
469 def get_derived_children(self, node):
470 children = node.all_children(None)
471 return [x for x in children if x.has_builder()]
472 def display(self, t) -> None:
473 if self.derived:
474 func = self.get_derived_children
475 else:
476 func = self.get_all_children
477 s = self.status and 2 or 0
478 SCons.Util.print_tree(t, func, prune=self.prune, showtags=s, lastChild=True, singleLineDraw=self.sLineDraw)
481 def python_version_string():
482 return sys.version.split()[0]
484 def python_version_unsupported(version=sys.version_info):
485 return version < minimum_python_version
487 def python_version_deprecated(version=sys.version_info):
488 return version < deprecated_python_version
491 class FakeOptionParser:
492 """A do-nothing option parser, used for the initial OptionsParser value.
494 During normal SCons operation, the OptionsParser is created right
495 away by the main() function. Certain test scripts however, can
496 introspect on different Tool modules, the initialization of which
497 can try to add a new, local option to an otherwise uninitialized
498 OptionsParser object. This allows that introspection to happen
499 without blowing up.
502 class FakeOptionValues:
503 def __getattr__(self, attr):
504 return None
506 values = FakeOptionValues()
508 # TODO: to quiet checkers, FakeOptionParser should also define
509 # raise_exception_on_error, preserve_unknown_options, largs and parse_args
511 def add_local_option(self, *args, **kw) -> "SConsOption":
512 pass
515 OptionsParser = FakeOptionParser()
517 def AddOption(*args, settable: bool = False, **kw) -> "SConsOption":
518 """Add a local option to the option parser - Public API.
520 If the *settable* parameter is true, the option will be included in the
521 list of settable options; all other keyword arguments are passed on to
522 :meth:`~SCons.Script.SConsOptions.SConsOptionParser.add_local_option`.
524 .. versionchanged:: 4.8.0
525 The *settable* parameter added to allow including the new option
526 to the table of options eligible to use :func:`SetOption`.
529 if 'default' not in kw:
530 kw['default'] = None
531 kw['settable'] = settable
532 result = OptionsParser.add_local_option(*args, **kw)
533 return result
535 def GetOption(name: str):
536 """Get the value from an option - Public API."""
537 return getattr(OptionsParser.values, name)
539 def SetOption(name: str, value):
540 """Set the value of an option - Public API."""
541 return OptionsParser.values.set_option(name, value)
543 def DebugOptions(json: Optional[str] = None) -> None:
544 """Specify options to SCons debug logic - Public API.
546 Currently only *json* is supported, which changes the JSON file
547 written to if the ``--debug=json`` command-line option is specified
548 to the value supplied.
550 .. versionadded:: 4.6.0
553 if json is not None:
554 json_node = SCons.Defaults.DefaultEnvironment().arg2nodes(json)
555 SCons.Util.stats.JSON_OUTPUT_FILE = json_node[0].get_abspath()
556 # Check if parent dir to JSON_OUTPUT_FILE exists
557 json_dir = os.path.dirname(SCons.Util.stats.JSON_OUTPUT_FILE)
558 try:
559 if not os.path.isdir(json_dir):
560 os.makedirs(json_dir, exist_ok=True)
561 # Now try to open file and see if you can..
562 with open(SCons.Util.stats.JSON_OUTPUT_FILE,'w') as js:
563 pass
564 except OSError as e:
565 raise SCons.Errors.UserError(f"Unable to create directory for JSON debug output file: {SCons.Util.stats.JSON_OUTPUT_FILE}")
568 def ValidateOptions(throw_exception: bool = False) -> None:
569 """Validate options passed to SCons on the command line.
571 Checks that all options given on the command line are known to this
572 instance of SCons. Call after all of the cli options have been set
573 up through :func:`AddOption` calls. For example, if you added an
574 option ``--xyz`` and you call SCons with ``--xyy`` you can cause
575 SCons to issue an error message and exit by calling this function.
577 Arguments:
578 throw_exception: if an invalid option is present on the command line,
579 raises an exception if this optional parameter evaluates true;
580 if false (the default), issue a message and exit with error status.
582 Raises:
583 SConsBadOptionError: If *throw_exception* is true and there are invalid
584 options on the command line.
586 .. versionadded:: 4.5.0
588 OptionsParser.raise_exception_on_error = throw_exception
589 OptionsParser.preserve_unknown_options = False
590 OptionsParser.parse_args(OptionsParser.largs, OptionsParser.values)
592 def PrintHelp(file=None, local_only: bool = False) -> None:
593 if local_only:
594 OptionsParser.print_local_option_help(file=file)
595 else:
596 OptionsParser.print_help(file=file)
599 # utility functions
601 def _scons_syntax_error(e) -> None:
602 """Handle syntax errors. Print out a message and show where the error
603 occurred.
605 etype, value, tb = sys.exc_info()
606 lines = traceback.format_exception_only(etype, value)
607 for line in lines:
608 sys.stderr.write(line+'\n')
609 sys.exit(2)
611 def find_deepest_user_frame(tb):
613 Find the deepest stack frame that is not part of SCons.
615 Input is a "pre-processed" stack trace in the form
616 returned by traceback.extract_tb() or traceback.extract_stack()
619 tb.reverse()
621 # find the deepest traceback frame that is not part
622 # of SCons:
623 for frame in tb:
624 filename = frame[0]
625 if filename.find(os.sep+'SCons'+os.sep) == -1:
626 return frame
627 return tb[0]
629 def _scons_user_error(e) -> None:
630 """Handle user errors. Print out a message and a description of the
631 error, along with the line number and routine where it occured.
632 The file and line number will be the deepest stack frame that is
633 not part of SCons itself.
635 global print_stacktrace
636 etype, value, tb = sys.exc_info()
637 if print_stacktrace:
638 traceback.print_exception(etype, value, tb)
639 filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_tb(tb))
640 sys.stderr.write("\nscons: *** %s\n" % value)
641 sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
642 sys.exit(2)
644 def _scons_user_warning(e) -> None:
645 """Handle user warnings. Print out a message and a description of
646 the warning, along with the line number and routine where it occured.
647 The file and line number will be the deepest stack frame that is
648 not part of SCons itself.
650 etype, value, tb = sys.exc_info()
651 filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_tb(tb))
652 sys.stderr.write("\nscons: warning: %s\n" % e)
653 sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
655 def _scons_internal_warning(e) -> None:
656 """Slightly different from _scons_user_warning in that we use the
657 *current call stack* rather than sys.exc_info() to get our stack trace.
658 This is used by the warnings framework to print warnings."""
659 filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_stack())
660 sys.stderr.write("\nscons: warning: %s\n" % e.args[0])
661 sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
663 def _scons_internal_error() -> None:
664 """Handle all errors but user errors. Print out a message telling
665 the user what to do in this case and print a normal trace.
667 print('internal error')
668 traceback.print_exc()
669 sys.exit(2)
671 def _SConstruct_exists(
672 dirname: str, repositories: List[str], filelist: List[str]
673 ) -> Optional[str]:
674 """Check that an SConstruct file exists in a directory.
676 Arguments:
677 dirname: the directory to search. If empty, look in cwd.
678 repositories: a list of repositories to search in addition to the
679 project directory tree.
680 filelist: names of SConstruct file(s) to search for.
681 If empty list, use the built-in list of names.
683 Returns:
684 The path to the located SConstruct file, or ``None``.
687 if not filelist:
688 filelist = KNOWN_SCONSTRUCT_NAMES
689 for file in filelist:
690 sfile = os.path.join(dirname, file)
691 if os.path.isfile(sfile):
692 return sfile
693 if not os.path.isabs(sfile):
694 for rep in repositories:
695 if os.path.isfile(os.path.join(rep, sfile)):
696 return sfile
697 return None
700 def _set_debug_values(options) -> None:
701 global print_memoizer, print_objects, print_stacktrace, print_time, \
702 print_action_timestamps, ENABLE_JSON
704 debug_values = options.debug
706 if "count" in debug_values:
707 # All of the object counts are within "if track_instances:" blocks,
708 # which get stripped when running optimized (with python -O or
709 # from compiled *.pyo files). Provide a warning if __debug__ is
710 # stripped, so it doesn't just look like --debug=count is broken.
711 enable_count = False
712 if __debug__: enable_count = True
713 if enable_count:
714 count_stats.enable(sys.stdout)
715 SCons.Debug.track_instances = True
716 else:
717 msg = "--debug=count is not supported when running SCons\n" + \
718 "\twith the python -O option or optimized (.pyo) modules."
719 SCons.Warnings.warn(SCons.Warnings.NoObjectCountWarning, msg)
720 if "dtree" in debug_values:
721 options.tree_printers.append(TreePrinter(derived=True))
722 options.debug_explain = "explain" in debug_values
723 if "findlibs" in debug_values:
724 SCons.Scanner.Prog.print_find_libs = "findlibs"
725 options.debug_includes = "includes" in debug_values
726 print_memoizer = "memoizer" in debug_values
727 if "memory" in debug_values:
728 memory_stats.enable(sys.stdout)
729 print_objects = ("objects" in debug_values)
730 if print_objects:
731 SCons.Debug.track_instances = True
732 if "presub" in debug_values:
733 SCons.Action.print_actions_presub = True
734 if "stacktrace" in debug_values:
735 print_stacktrace = True
736 if "stree" in debug_values:
737 options.tree_printers.append(TreePrinter(status=True))
738 if "time" in debug_values:
739 print_time = True
740 time_stats.enable(sys.stdout)
741 time_stats.enable(sys.stdout)
742 if "action-timestamps" in debug_values:
743 print_time = True
744 print_action_timestamps = True
745 if "tree" in debug_values:
746 options.tree_printers.append(TreePrinter())
747 if "prepare" in debug_values:
748 SCons.Taskmaster.print_prepare = True
749 if "duplicate" in debug_values:
750 SCons.Node.print_duplicate = True
751 if "json" in debug_values:
752 ENABLE_JSON = True
753 if "sconscript" in debug_values:
754 SCons.Debug.sconscript_trace = True
756 def _create_path(plist):
757 path = '.'
758 for d in plist:
759 if os.path.isabs(d):
760 path = d
761 else:
762 path = path + '/' + d
763 return path
765 def _load_site_scons_dir(topdir, site_dir_name=None):
766 """Load the site directory under topdir.
768 If a site dir name is supplied use it, else use default "site_scons"
769 Prepend site dir to sys.path.
770 If a "site_tools" subdir exists, prepend to toolpath.
771 Import "site_init.py" from site dir if it exists.
773 if site_dir_name:
774 err_if_not_found = True # user specified: err if missing
775 else:
776 site_dir_name = "site_scons"
777 err_if_not_found = False # scons default: okay to be missing
778 site_dir = os.path.join(topdir, site_dir_name)
780 if not os.path.exists(site_dir):
781 if err_if_not_found:
782 raise SCons.Errors.UserError("site dir %s not found." % site_dir)
783 return
784 sys.path.insert(0, os.path.abspath(site_dir))
786 site_init_filename = "site_init.py"
787 site_init_modname = "site_init"
788 site_tools_dirname = "site_tools"
789 site_init_file = os.path.join(site_dir, site_init_filename)
790 site_tools_dir = os.path.join(site_dir, site_tools_dirname)
792 if os.path.exists(site_tools_dir):
793 SCons.Tool.DefaultToolpath.insert(0, os.path.abspath(site_tools_dir))
795 if not os.path.exists(site_init_file):
796 return
798 # "import" the site_init.py file into the SCons.Script namespace.
799 # This is a variant on the basic Python import flow in that the globals
800 # dict for the compile step is prepopulated from the SCons.Script
801 # module object; on success the SCons.Script globals are refilled
802 # from the site_init globals so it all appears in SCons.Script
803 # instead of as a separate module.
804 try:
805 try:
806 m = sys.modules['SCons.Script']
807 except KeyError:
808 fmt = 'cannot import {}: missing SCons.Script module'
809 raise SCons.Errors.InternalError(fmt.format(site_init_file))
811 spec = importlib.util.spec_from_file_location(site_init_modname, site_init_file)
812 site_m = {
813 "__file__": spec.origin,
814 "__name__": spec.name,
815 "__doc__": None,
817 re_dunder = re.compile(r"__[^_]+__")
818 # update site dict with all but magic (dunder) methods
819 for k, v in m.__dict__.items():
820 if not re_dunder.match(k):
821 site_m[k] = v
823 with open(spec.origin) as f:
824 code = f.read()
825 try:
826 codeobj = compile(code, spec.name, "exec")
827 exec(codeobj, site_m)
828 except KeyboardInterrupt:
829 raise
830 except Exception:
831 fmt = "*** Error loading site_init file {}:\n"
832 sys.stderr.write(fmt.format(site_init_file))
833 raise
834 else:
835 # now refill globals with site_init's symbols
836 for k, v in site_m.items():
837 if not re_dunder.match(k):
838 m.__dict__[k] = v
839 except KeyboardInterrupt:
840 raise
841 except Exception:
842 fmt = "*** cannot import site init file {}:\n"
843 sys.stderr.write(fmt.format(site_init_file))
844 raise
847 def _load_all_site_scons_dirs(topdir, verbose: bool=False) -> None:
848 """Load all of the predefined site_scons dir.
849 Order is significant; we load them in order from most generic
850 (machine-wide) to most specific (topdir).
851 The verbose argument is only for testing.
853 platform = SCons.Platform.platform_default()
855 def homedir(d):
856 return os.path.expanduser('~/'+d)
858 if platform == 'win32' or platform == 'cygwin':
859 sysdirs=[
860 os.path.expandvars('%AllUsersProfile%\\scons'),
861 # TODO older path, kept for compat
862 os.path.expandvars('%AllUsersProfile%\\Application Data\\scons'),
863 os.path.expandvars('%LocalAppData%\\scons')]
864 appdatadir = os.path.expandvars('%AppData%\\scons')
865 if appdatadir not in sysdirs:
866 sysdirs.append(appdatadir)
867 sysdirs.append(homedir('.scons'))
869 elif platform == 'darwin': # MacOS X
870 sysdirs=['/Library/Application Support/SCons',
871 '/opt/local/share/scons', # (for MacPorts)
872 '/sw/share/scons', # (for Fink)
873 homedir('Library/Application Support/SCons'),
874 homedir('.scons')]
875 elif platform == 'sunos': # Solaris
876 sysdirs=['/opt/sfw/scons',
877 '/usr/share/scons',
878 homedir('.scons')]
879 else: # Linux, HPUX, etc.
880 # assume posix-like, i.e. platform == 'posix'
881 sysdirs=['/usr/share/scons',
882 homedir('.scons')]
884 dirs = sysdirs + [topdir]
885 for d in dirs:
886 if verbose: # this is used by unit tests.
887 print("Loading site dir ", d)
888 _load_site_scons_dir(d)
890 def test_load_all_site_scons_dirs(d) -> None:
891 _load_all_site_scons_dirs(d, True)
893 def version_string(label, module):
894 version = module.__version__
895 build = module.__build__
896 if build:
897 if build[0] != '.':
898 build = '.' + build
899 version = version + build
900 fmt = "\t%s: v%s, %s, by %s on %s\n"
901 return fmt % (label,
902 version,
903 module.__date__,
904 module.__developer__,
905 module.__buildsys__)
907 def path_string(label, module) -> str:
908 path = module.__path__
909 return "\t%s path: %s\n"%(label,path)
911 def _main(parser):
912 global exit_status
913 global this_build_status
915 options = parser.values
917 # Here's where everything really happens.
919 # First order of business: set up default warnings and then
920 # handle the user's warning options, so that we can issue (or
921 # suppress) appropriate warnings about anything that might happen,
922 # as configured by the user.
924 default_warnings = [ SCons.Warnings.WarningOnByDefault,
925 SCons.Warnings.DeprecatedWarning,
928 for warning in default_warnings:
929 SCons.Warnings.enableWarningClass(warning)
930 SCons.Warnings._warningOut = _scons_internal_warning
931 SCons.Warnings.process_warn_strings(options.warn)
933 # Now that we have the warnings configuration set up, we can actually
934 # issue (or suppress) any warnings about warning-worthy things that
935 # occurred while the command-line options were getting parsed.
936 try:
937 dw = options.delayed_warnings
938 except AttributeError:
939 pass
940 else:
941 delayed_warnings.extend(dw)
942 for warning_type, message in delayed_warnings:
943 SCons.Warnings.warn(warning_type, message)
945 if not SCons.Platform.virtualenv.virtualenv_enabled_by_default:
946 if options.enable_virtualenv:
947 SCons.Platform.virtualenv.enable_virtualenv = True
949 if options.ignore_virtualenv:
950 SCons.Platform.virtualenv.ignore_virtualenv = True
952 if options.diskcheck:
953 SCons.Node.FS.set_diskcheck(options.diskcheck)
955 # Next, we want to create the FS object that represents the outside
956 # world's file system, as that's central to a lot of initialization.
957 # To do this, however, we need to be in the directory from which we
958 # want to start everything, which means first handling any relevant
959 # options that might cause us to chdir somewhere (-C, -D, -U, -u).
960 if options.directory:
961 script_dir = os.path.abspath(_create_path(options.directory))
962 else:
963 script_dir = os.getcwd()
965 target_top = None
966 if options.climb_up:
967 target_top = '.' # directory to prepend to targets
968 while script_dir and not _SConstruct_exists(
969 script_dir, options.repository, options.file
971 script_dir, last_part = os.path.split(script_dir)
972 if last_part:
973 target_top = os.path.join(last_part, target_top)
974 else:
975 script_dir = ''
977 if script_dir and script_dir != os.getcwd():
978 if not options.silent:
979 display("scons: Entering directory `%s'" % script_dir)
980 try:
981 os.chdir(script_dir)
982 except OSError:
983 sys.stderr.write("Could not change directory to %s\n" % script_dir)
985 # Now that we're in the top-level SConstruct directory, go ahead
986 # and initialize the FS object that represents the file system,
987 # and make it the build engine default.
988 fs = SCons.Node.FS.get_default_fs()
990 for rep in options.repository:
991 fs.Repository(rep)
993 # Now that we have the FS object, the next order of business is to
994 # check for an SConstruct file (or other specified config file).
995 # If there isn't one, we can bail before doing any more work.
996 scripts = []
997 if options.file:
998 scripts.extend(options.file)
999 if not scripts:
1000 sfile = _SConstruct_exists("", options.repository, options.file)
1001 if sfile:
1002 scripts.append(sfile)
1004 if not scripts:
1005 if options.help:
1006 # There's no SConstruct, but they specified -h.
1007 # Give them the options usage now, before we fail
1008 # trying to read a non-existent SConstruct file.
1009 raise SConsPrintHelpException
1010 raise SCons.Errors.UserError("No SConstruct file found.")
1012 if scripts[0] == "-":
1013 d = fs.getcwd()
1014 else:
1015 d = fs.File(scripts[0]).dir
1016 fs.set_SConstruct_dir(d)
1018 _set_debug_values(options)
1019 SCons.Node.implicit_cache = options.implicit_cache
1020 SCons.Node.implicit_deps_changed = options.implicit_deps_changed
1021 SCons.Node.implicit_deps_unchanged = options.implicit_deps_unchanged
1023 if options.no_exec:
1024 SCons.SConf.dryrun = 1
1025 SCons.Action.execute_actions = None
1026 if options.question:
1027 SCons.SConf.dryrun = 1
1028 if options.clean:
1029 SCons.SConf.SetBuildType('clean')
1030 if options.help:
1031 SCons.SConf.SetBuildType('help')
1032 SCons.SConf.SetCacheMode(options.config)
1033 SCons.SConf.SetProgressDisplay(progress_display)
1035 if options.no_progress or options.silent:
1036 progress_display.set_mode(0)
1038 # if site_dir unchanged from default None, neither --site-dir
1039 # nor --no-site-dir was seen, use SCons default
1040 if options.site_dir is None:
1041 _load_all_site_scons_dirs(d.get_internal_path())
1042 elif options.site_dir: # if a dir was set, use it
1043 _load_site_scons_dir(d.get_internal_path(), options.site_dir)
1045 if options.include_dir:
1046 sys.path = options.include_dir + sys.path
1048 # If we're about to start SCons in the interactive mode,
1049 # inform the FS about this right here. Else, the release_target_info
1050 # method could get called on some nodes, like the used "gcc" compiler,
1051 # when using the Configure methods within the SConscripts.
1052 # This would then cause subtle bugs, as already happened in #2971.
1053 if options.interactive:
1054 SCons.Node.interactive = True
1055 # That should cover (most of) the options.
1056 # Next, set up the variables that hold command-line arguments,
1057 # so the SConscript files that we read and execute have access to them.
1058 # TODO: for options defined via AddOption which take space-separated
1059 # option-args, the option-args will collect into targets here,
1060 # because we don't yet know to do any different.
1061 targets = []
1062 xmit_args = []
1063 for a in parser.largs:
1064 # Skip so-far unrecognized options, and empty string args
1065 if a.startswith('-') or a in ('', '""', "''"):
1066 continue
1067 if '=' in a:
1068 xmit_args.append(a)
1069 else:
1070 targets.append(a)
1071 SCons.Script._Add_Targets(targets + parser.rargs)
1072 SCons.Script._Add_Arguments(xmit_args)
1074 # If stdout is not a tty, replace it with a wrapper object to call flush
1075 # after every write.
1077 # Tty devices automatically flush after every newline, so the replacement
1078 # isn't necessary. Furthermore, if we replace sys.stdout, the readline
1079 # module will no longer work. This affects the behavior during
1080 # --interactive mode. --interactive should only be used when stdin and
1081 # stdout refer to a tty.
1082 if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty():
1083 sys.stdout = SCons.Util.Unbuffered(sys.stdout)
1084 if not hasattr(sys.stderr, 'isatty') or not sys.stderr.isatty():
1085 sys.stderr = SCons.Util.Unbuffered(sys.stderr)
1087 memory_stats.append('before reading SConscript files:')
1088 count_stats.append(('pre-', 'read'))
1090 # And here's where we (finally) read the SConscript files.
1092 progress_display("scons: Reading SConscript files ...")
1094 if print_time:
1095 start_time = time.time()
1096 try:
1097 for script in scripts:
1098 SCons.Script._SConscript._SConscript(fs, script)
1099 except SCons.Errors.StopError as e:
1100 # We had problems reading an SConscript file, such as it
1101 # couldn't be copied in to the VariantDir. Since we're just
1102 # reading SConscript files and haven't started building
1103 # things yet, stop regardless of whether they used -i or -k
1104 # or anything else.
1105 revert_io()
1106 sys.stderr.write("scons: *** %s Stop.\n" % e)
1107 sys.exit(2)
1108 if print_time:
1109 global sconscript_time
1110 sconscript_time = time.time() - start_time
1112 progress_display("scons: done reading SConscript files.")
1114 memory_stats.append('after reading SConscript files:')
1115 count_stats.append(('post-', 'read'))
1117 # Re-{enable,disable} warnings in case they disabled some in
1118 # the SConscript file.
1120 # We delay enabling the PythonVersionWarning class until here so that,
1121 # if they explicitly disabled it in either in the command line or in
1122 # $SCONSFLAGS, or in the SConscript file, then the search through
1123 # the list of deprecated warning classes will find that disabling
1124 # first and not issue the warning.
1125 #SCons.Warnings.enableWarningClass(SCons.Warnings.PythonVersionWarning)
1126 SCons.Warnings.process_warn_strings(options.warn)
1128 # Now that we've read the SConscript files, we can check for the
1129 # warning about deprecated Python versions--delayed until here
1130 # in case they disabled the warning in the SConscript files.
1131 if python_version_deprecated():
1132 deprecated_version_string = ".".join(map(str, deprecated_python_version))
1133 msg = (
1134 f"Support for Python older than {deprecated_version_string}"
1135 f" is deprecated ({python_version_string()} detected).\n"
1136 " If this will cause hardship, contact scons-dev@scons.org"
1138 SCons.Warnings.warn(SCons.Warnings.PythonVersionWarning, msg)
1140 if not options.help:
1141 # [ ] Clarify why we need to create Builder here at all, and
1142 # why it is created in DefaultEnvironment
1143 # https://bitbucket.org/scons/scons/commits/d27a548aeee8ad5e67ea75c2d19a7d305f784e30
1144 if SCons.SConf.NeedConfigHBuilder():
1145 SCons.SConf.CreateConfigHBuilder(SCons.Defaults.DefaultEnvironment())
1147 # Now re-parse the command-line options (any to the left of a '--'
1148 # argument, that is) with any user-defined command-line options that
1149 # the SConscript files may have added to the parser object. This will
1150 # emit the appropriate error message and exit if any unknown option
1151 # was specified on the command line.
1153 parser.preserve_unknown_options = False
1154 parser.parse_args(parser.largs, options)
1156 if options.help:
1157 help_text = SCons.Script.help_text
1158 if help_text is None:
1159 # They specified -h, but there was no Help() inside the
1160 # SConscript files. Give them the options usage.
1161 raise SConsPrintHelpException
1162 else:
1163 print(help_text)
1164 print("Use scons -H for help about SCons built-in command-line options.")
1165 exit_status = 0
1166 return
1168 # Change directory to the top-level SConstruct directory, then tell
1169 # the Node.FS subsystem that we're all done reading the SConscript
1170 # files and calling Repository() and VariantDir() and changing
1171 # directories and the like, so it can go ahead and start memoizing
1172 # the string values of file system nodes.
1174 fs.chdir(fs.Top)
1176 SCons.Node.FS.save_strings(1)
1178 # Now that we've read the SConscripts we can set the options
1179 # that are SConscript settable:
1180 SCons.Node.implicit_cache = options.implicit_cache
1181 SCons.Node.FS.set_duplicate(options.duplicate)
1182 fs.set_max_drift(options.max_drift)
1184 SCons.Taskmaster.Job.explicit_stack_size = options.stack_size
1186 # Hash format and chunksize are set late to support SetOption being called
1187 # in a SConscript or SConstruct file.
1188 SCons.Util.set_hash_format(options.hash_format)
1189 if options.md5_chunksize:
1190 SCons.Node.FS.File.hash_chunksize = options.md5_chunksize * 1024
1192 platform = SCons.Platform.platform_module()
1194 if options.interactive:
1195 SCons.Script.Interactive.interact(fs, OptionsParser, options,
1196 targets, target_top)
1198 else:
1200 # Build the targets
1201 nodes = _build_targets(fs, options, targets, target_top)
1202 if not nodes:
1203 revert_io()
1204 print('Found nothing to build')
1205 exit_status = 2
1207 def _build_targets(fs, options, targets, target_top):
1209 global this_build_status
1210 this_build_status = 0
1212 progress_display.set_mode(not (options.no_progress or options.silent))
1213 display.set_mode(not options.silent)
1214 SCons.Action.print_actions = not options.silent
1215 SCons.Action.execute_actions = not options.no_exec
1216 SCons.Node.do_store_info = not options.no_exec
1217 SCons.SConf.dryrun = options.no_exec
1219 if options.diskcheck:
1220 SCons.Node.FS.set_diskcheck(options.diskcheck)
1222 SCons.CacheDir.cache_enabled = not options.cache_disable
1223 SCons.CacheDir.cache_readonly = options.cache_readonly
1224 SCons.CacheDir.cache_debug = options.cache_debug
1225 SCons.CacheDir.cache_force = options.cache_force
1226 SCons.CacheDir.cache_show = options.cache_show
1228 if options.no_exec:
1229 CleanTask.execute = CleanTask.show
1230 else:
1231 CleanTask.execute = CleanTask.remove
1233 lookup_top = None
1234 if targets or SCons.Script.BUILD_TARGETS != SCons.Script._build_plus_default:
1235 # They specified targets on the command line or modified
1236 # BUILD_TARGETS in the SConscript file(s), so if they used -u,
1237 # -U or -D, we have to look up targets relative to the top,
1238 # but we build whatever they specified.
1239 if target_top:
1240 lookup_top = fs.Dir(target_top)
1241 target_top = None
1243 targets = SCons.Script.BUILD_TARGETS
1244 else:
1245 # There are no targets specified on the command line,
1246 # so if they used -u, -U or -D, we may have to restrict
1247 # what actually gets built.
1248 d = None
1249 if target_top:
1250 if options.climb_up == 1:
1251 # -u, local directory and below
1252 target_top = fs.Dir(target_top)
1253 lookup_top = target_top
1254 elif options.climb_up == 2:
1255 # -D, all Default() targets
1256 target_top = None
1257 lookup_top = None
1258 elif options.climb_up == 3:
1259 # -U, local SConscript Default() targets
1260 target_top = fs.Dir(target_top)
1261 def check_dir(x, target_top=target_top):
1262 if hasattr(x, 'cwd') and x.cwd is not None:
1263 cwd = x.cwd.srcnode()
1264 return cwd == target_top
1265 else:
1266 # x doesn't have a cwd, so it's either not a target,
1267 # or not a file, so go ahead and keep it as a default
1268 # target and let the engine sort it out:
1269 return 1
1270 d = [tgt for tgt in SCons.Script.DEFAULT_TARGETS if check_dir(tgt)]
1271 SCons.Script.DEFAULT_TARGETS[:] = d
1272 target_top = None
1273 lookup_top = None
1275 targets = SCons.Script._Get_Default_Targets(d, fs)
1277 if not targets:
1278 sys.stderr.write("scons: *** No targets specified and no Default() targets found. Stop.\n")
1279 return None
1281 def Entry(x, ltop=lookup_top, ttop=target_top, fs=fs):
1282 if isinstance(x, SCons.Node.Node):
1283 node = x
1284 else:
1285 node = None
1286 # Why would ltop be None? Unfortunately this happens.
1287 if ltop is None: ltop = ''
1288 # Curdir becomes important when SCons is called with -u, -C,
1289 # or similar option that changes directory, and so the paths
1290 # of targets given on the command line need to be adjusted.
1291 curdir = os.path.join(os.getcwd(), str(ltop))
1292 for lookup in SCons.Node.arg2nodes_lookups:
1293 node = lookup(x, curdir=curdir)
1294 if node is not None:
1295 break
1296 if node is None:
1297 node = fs.Entry(x, directory=ltop, create=1)
1298 if ttop and not node.is_under(ttop):
1299 if isinstance(node, SCons.Node.FS.Dir) and ttop.is_under(node):
1300 node = ttop
1301 else:
1302 node = None
1303 return node
1305 nodes = [_f for _f in map(Entry, targets) if _f]
1307 task_class = BuildTask # default action is to build targets
1308 opening_message = "Building targets ..."
1309 closing_message = "done building targets."
1310 if options.keep_going:
1311 failure_message = "done building targets (errors occurred during build)."
1312 else:
1313 failure_message = "building terminated because of errors."
1314 if options.question:
1315 task_class = QuestionTask
1316 try:
1317 if options.clean:
1318 task_class = CleanTask
1319 opening_message = "Cleaning targets ..."
1320 closing_message = "done cleaning targets."
1321 if options.keep_going:
1322 failure_message = "done cleaning targets (errors occurred during clean)."
1323 else:
1324 failure_message = "cleaning terminated because of errors."
1325 except AttributeError:
1326 pass
1328 task_class.progress = ProgressObject
1330 if options.random:
1331 def order(dependencies):
1332 """Randomize the dependencies."""
1333 import random
1334 random.shuffle(dependencies)
1335 return dependencies
1336 else:
1337 def order(dependencies):
1338 """Leave the order of dependencies alone."""
1339 return dependencies
1341 taskmaster = SCons.Taskmaster.Taskmaster(nodes, task_class, order, options.taskmastertrace_file)
1343 # Let the BuildTask objects get at the options to respond to the
1344 # various print_* settings, tree_printer list, etc.
1345 BuildTask.options = options
1347 is_pypy = platform.python_implementation() == 'PyPy'
1348 # As of 3.7, python removed support for threadless platforms.
1349 # See https://www.python.org/dev/peps/pep-0011/
1350 is_37_or_later = sys.version_info >= (3, 7)
1351 # python_has_threads = sysconfig.get_config_var('WITH_THREAD') or is_pypy or is_37_or_later
1353 # As of python 3.4 threading has a dummy_threading module for use when there is no threading
1354 # it's get_ident() will allways return -1, while real threading modules get_ident() will
1355 # always return a positive integer
1356 python_has_threads = threading.get_ident() != -1
1357 # to check if python configured with threads.
1358 global num_jobs
1359 num_jobs = options.num_jobs
1360 jobs = SCons.Taskmaster.Job.Jobs(num_jobs, taskmaster)
1361 if num_jobs > 1:
1362 msg = None
1363 if jobs.num_jobs == 1 or not python_has_threads:
1364 msg = "parallel builds are unsupported by this version of Python;\n" + \
1365 "\tignoring -j or num_jobs option.\n"
1366 if msg:
1367 SCons.Warnings.warn(SCons.Warnings.NoParallelSupportWarning, msg)
1369 memory_stats.append('before building targets:')
1370 count_stats.append(('pre-', 'build'))
1372 def jobs_postfunc(
1373 jobs=jobs,
1374 options=options,
1375 closing_message=closing_message,
1376 failure_message=failure_message
1377 ) -> None:
1378 if jobs.were_interrupted():
1379 if not options.no_progress and not options.silent:
1380 sys.stderr.write("scons: Build interrupted.\n")
1381 global exit_status
1382 global this_build_status
1383 exit_status = 2
1384 this_build_status = 2
1386 if this_build_status:
1387 progress_display("scons: " + failure_message)
1388 else:
1389 progress_display("scons: " + closing_message)
1390 if not options.no_exec:
1391 if jobs.were_interrupted():
1392 progress_display("scons: writing .sconsign file.")
1393 SCons.SConsign.write()
1395 progress_display("scons: " + opening_message)
1396 jobs.run(postfunc = jobs_postfunc)
1398 memory_stats.append('after building targets:')
1399 count_stats.append(('post-', 'build'))
1401 return nodes
1403 def _exec_main(parser, values) -> None:
1404 sconsflags = os.environ.get('SCONSFLAGS', '')
1405 all_args = sconsflags.split() + sys.argv[1:]
1407 options, args = parser.parse_args(all_args, values)
1409 if isinstance(options.debug, list) and "pdb" in options.debug:
1410 import pdb
1412 class SConsPdb(pdb.Pdb):
1413 """Specialization of Pdb to help find SConscript files."""
1415 def lookupmodule(self, filename: str) -> Optional[str]:
1416 """Helper function for break/clear parsing -- SCons version.
1418 Translates (possibly incomplete) file or module name
1419 into an absolute file name. The "possibly incomplete"
1420 means adding a ``.py`` suffix if not present, which breaks
1421 picking breakpoints in sconscript files, which often don't
1422 have a suffix. This version fixes for some known names of
1423 sconscript files that don't have the suffix.
1425 .. versionadded:: 4.6.0
1427 .. versionchanged:: 4.8.0
1428 The additional name ``SCsub`` (with spelling variants)
1429 is also recognized - Godot uses this name.
1431 if os.path.isabs(filename) and os.path.exists(filename):
1432 return filename
1433 f = os.path.join(sys.path[0], filename)
1434 if os.path.exists(f) and self.canonic(f) == self.mainpyfile:
1435 return f
1436 root, ext = os.path.splitext(filename)
1437 base = os.path.split(filename)[-1]
1438 if ext == '' and base not in KNOWN_SCONSCRIPTS: # SCons mod
1439 filename = filename + '.py'
1440 if os.path.isabs(filename):
1441 return filename
1442 for dirname in sys.path:
1443 while os.path.islink(dirname):
1444 dirname = os.readlink(dirname)
1445 fullname = os.path.join(dirname, filename)
1446 if os.path.exists(fullname):
1447 return fullname
1448 return None
1450 SConsPdb().runcall(_main, parser)
1452 elif options.profile_file:
1453 from cProfile import Profile
1455 prof = Profile()
1456 try:
1457 prof.runcall(_main, parser)
1458 finally:
1459 prof.dump_stats(options.profile_file)
1461 else:
1462 _main(parser)
1465 def main() -> None:
1466 global OptionsParser
1467 global exit_status
1468 global first_command_start
1469 global ENABLE_JSON
1471 # Check up front for a Python version we do not support. We
1472 # delay the check for deprecated Python versions until later,
1473 # after the SConscript files have been read, in case they
1474 # disable that warning.
1475 if python_version_unsupported():
1476 msg = "scons: *** SCons version %s does not run under Python version %s.\n"
1477 sys.stderr.write(msg % (SConsVersion, python_version_string()))
1478 sys.stderr.write("scons: *** Minimum Python version is %d.%d.%d\n" %minimum_python_version)
1479 sys.exit(1)
1481 try:
1482 import threading
1483 except ImportError:
1484 msg = "scons: *** SCons version %s requires a Python interpreter with support for the `threading` package"
1485 sys.stderr.write(msg % SConsVersion)
1486 sys.exit(1)
1488 parts = ["SCons by Steven Knight et al.:\n"]
1489 try:
1490 import SCons
1491 parts.append(version_string("SCons", SCons))
1492 except (ImportError, AttributeError):
1493 # On Windows there is no scons.py, so there is no
1494 # __main__.__version__, hence there is no script version.
1495 pass
1496 parts.append(path_string("SCons", SCons))
1497 parts.append(SCons.__copyright__)
1498 version = ''.join(parts)
1500 from . import SConsOptions
1501 parser = SConsOptions.Parser(version)
1502 values = SConsOptions.SConsValues(parser.get_default_values())
1504 OptionsParser = parser
1506 try:
1507 try:
1508 _exec_main(parser, values)
1509 finally:
1510 revert_io()
1511 except SystemExit as s:
1512 if s:
1513 exit_status = s.code
1514 except KeyboardInterrupt:
1515 print("scons: Build interrupted.")
1516 sys.exit(2)
1517 except SyntaxError as e:
1518 _scons_syntax_error(e)
1519 except SCons.Errors.InternalError:
1520 _scons_internal_error()
1521 except SCons.Errors.UserError as e:
1522 _scons_user_error(e)
1523 except SConsPrintHelpException:
1524 parser.print_help()
1525 exit_status = 0
1526 except SCons.Errors.BuildError as e:
1527 print(e)
1528 exit_status = e.exitstatus
1529 except:
1530 # An exception here is likely a builtin Python exception Python
1531 # code in an SConscript file. Show them precisely what the
1532 # problem was and where it happened.
1533 SCons.Script._SConscript.SConscript_exception()
1534 sys.exit(2)
1536 memory_stats.print_stats()
1537 count_stats.print_stats()
1539 if print_objects:
1540 SCons.Debug.listLoggedInstances('*')
1541 #SCons.Debug.dumpLoggedInstances('*')
1543 if print_memoizer:
1544 SCons.Memoize.Dump("Memoizer (memory cache) hits and misses:")
1546 # Dump any development debug info that may have been enabled.
1547 # These are purely for internal debugging during development, so
1548 # there's no need to control them with --debug= options; they're
1549 # controlled by changing the source code.
1550 SCons.Debug.dump_caller_counts()
1551 SCons.Taskmaster.dump_stats()
1553 if print_time:
1554 total_time = time.time() - SCons.Script.start_time
1555 if num_jobs == 1:
1556 ct = cumulative_command_time
1557 else:
1558 if last_command_end is None or first_command_start is None:
1559 ct = 0.0
1560 else:
1561 ct = last_command_end - first_command_start
1562 scons_time = total_time - sconscript_time - ct
1563 print("Total build time: %f seconds"%total_time)
1564 print("Total SConscript file execution time: %f seconds"%sconscript_time)
1565 print("Total SCons execution time: %f seconds"%scons_time)
1566 print("Total command execution time: %f seconds"%ct)
1567 time_stats.total_times(total_time, sconscript_time, scons_time, ct)
1570 if ENABLE_JSON:
1571 write_scons_stats_file()
1573 sys.exit(exit_status)
1575 # Local Variables:
1576 # tab-width:4
1577 # indent-tabs-mode:nil
1578 # End:
1579 # vim: set expandtab tabstop=4 shiftwidth=4: