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