Remove redundant code
[scons.git] / SCons / Script / SConscript.py
blob85070ab0532a48ef661c7488ad02bdf746695e19
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 """This module defines the Python API provided to SConscript files."""
26 import SCons
27 import SCons.Action
28 import SCons.Builder
29 import SCons.Defaults
30 import SCons.Environment
31 import SCons.Errors
32 import SCons.Node
33 import SCons.Node.Alias
34 import SCons.Node.FS
35 import SCons.Platform
36 import SCons.SConf
37 import SCons.Tool
38 from SCons.Util import is_List, is_String, is_Dict, flatten
39 from SCons.Node import SConscriptNodes
40 from . import Main
42 import os
43 import os.path
44 import re
45 import sys
46 import traceback
47 import time
49 class SConscriptReturn(Exception):
50 pass
52 launch_dir = os.path.abspath(os.curdir)
54 GlobalDict = None
56 # global exports set by Export():
57 global_exports = {}
59 # chdir flag
60 sconscript_chdir: bool = True
62 def get_calling_namespaces():
63 """Return the locals and globals for the function that called
64 into this module in the current call stack."""
65 try: 1//0
66 except ZeroDivisionError:
67 # Don't start iterating with the current stack-frame to
68 # prevent creating reference cycles (f_back is safe).
69 frame = sys.exc_info()[2].tb_frame.f_back
71 # Find the first frame that *isn't* from this file. This means
72 # that we expect all of the SCons frames that implement an Export()
73 # or SConscript() call to be in this file, so that we can identify
74 # the first non-Script.SConscript frame as the user's local calling
75 # environment, and the locals and globals dictionaries from that
76 # frame as the calling namespaces. See the comment below preceding
77 # the DefaultEnvironmentCall block for even more explanation.
78 while frame.f_globals.get("__name__") == __name__:
79 frame = frame.f_back
81 return frame.f_locals, frame.f_globals
84 def compute_exports(exports):
85 """Compute a dictionary of exports given one of the parameters
86 to the Export() function or the exports argument to SConscript()."""
88 loc, glob = get_calling_namespaces()
90 retval = {}
91 try:
92 for export in exports:
93 if is_Dict(export):
94 retval.update(export)
95 else:
96 try:
97 retval[export] = loc[export]
98 except KeyError:
99 retval[export] = glob[export]
100 except KeyError as x:
101 raise SCons.Errors.UserError("Export of non-existent variable '%s'"%x)
103 return retval
105 class Frame:
106 """A frame on the SConstruct/SConscript call stack"""
107 def __init__(self, fs, exports, sconscript) -> None:
108 self.globals = BuildDefaultGlobals()
109 self.retval = None
110 self.prev_dir = fs.getcwd()
111 self.exports = compute_exports(exports) # exports from the calling SConscript
112 # make sure the sconscript attr is a Node.
113 if isinstance(sconscript, SCons.Node.Node):
114 self.sconscript = sconscript
115 elif sconscript == '-':
116 self.sconscript = None
117 else:
118 self.sconscript = fs.File(str(sconscript))
120 # the SConstruct/SConscript call stack:
121 call_stack = []
123 # For documentation on the methods in this file, see the scons man-page
125 def Return(*vars, **kw):
126 retval = []
127 try:
128 fvars = flatten(vars)
129 for var in fvars:
130 for v in var.split():
131 retval.append(call_stack[-1].globals[v])
132 except KeyError as x:
133 raise SCons.Errors.UserError("Return of non-existent variable '%s'"%x)
135 if len(retval) == 1:
136 call_stack[-1].retval = retval[0]
137 else:
138 call_stack[-1].retval = tuple(retval)
140 stop = kw.get('stop', True)
142 if stop:
143 raise SConscriptReturn
146 stack_bottom = '% Stack boTTom %' # hard to define a variable w/this name :)
148 def handle_missing_SConscript(f: str, must_exist: bool = True) -> None:
149 """Take appropriate action on missing file in SConscript() call.
151 Print a warning or raise an exception on missing file, unless
152 missing is explicitly allowed by the *must_exist* parameter or by
153 a global flag.
155 Args:
156 f: path to missing configuration file
157 must_exist: if true (the default), fail. If false
158 do nothing, allowing a build to declare it's okay to be missing.
160 Raises:
161 UserError: if *must_exist* is true or if global
162 :data:`SCons.Script._no_missing_sconscript` is true.
164 .. versionchanged: 4.6.0
165 Changed default from False.
167 if not must_exist: # explicitly set False: ok
168 return
169 if not SCons.Script._no_missing_sconscript: # system default changed: ok
170 return
171 msg = f"missing SConscript file {f.get_internal_path()!r}"
172 raise SCons.Errors.UserError(msg)
175 def _SConscript(fs, *files, **kw):
176 top = fs.Top
177 sd = fs.SConstruct_dir.rdir()
178 exports = kw.get('exports', [])
180 # evaluate each SConscript file
181 results = []
182 for fn in files:
183 call_stack.append(Frame(fs, exports, fn))
184 old_sys_path = sys.path
185 try:
186 SCons.Script.sconscript_reading = SCons.Script.sconscript_reading + 1
187 if fn == "-":
188 exec(sys.stdin.read(), call_stack[-1].globals)
189 else:
190 if isinstance(fn, SCons.Node.Node):
191 f = fn
192 else:
193 f = fs.File(str(fn))
194 _file_ = None
195 SConscriptNodes.add(f)
197 # Change directory to the top of the source
198 # tree to make sure the os's cwd and the cwd of
199 # fs match so we can open the SConscript.
200 fs.chdir(top, change_os_dir=True)
201 if f.rexists():
202 actual = f.rfile()
203 _file_ = open(actual.get_abspath(), "rb")
204 elif f.srcnode().rexists():
205 actual = f.srcnode().rfile()
206 _file_ = open(actual.get_abspath(), "rb")
207 elif f.has_src_builder():
208 # The SConscript file apparently exists in a source
209 # code management system. Build it, but then clear
210 # the builder so that it doesn't get built *again*
211 # during the actual build phase.
212 f.build()
213 f.built()
214 f.builder_set(None)
215 if f.exists():
216 _file_ = open(f.get_abspath(), "rb")
217 if _file_:
218 # Chdir to the SConscript directory. Use a path
219 # name relative to the SConstruct file so that if
220 # we're using the -f option, we're essentially
221 # creating a parallel SConscript directory structure
222 # in our local directory tree.
224 # XXX This is broken for multiple-repository cases
225 # where the SConstruct and SConscript files might be
226 # in different Repositories. For now, cross that
227 # bridge when someone comes to it.
228 try:
229 src_dir = kw['src_dir']
230 except KeyError:
231 ldir = fs.Dir(f.dir.get_path(sd))
232 else:
233 ldir = fs.Dir(src_dir)
234 if not ldir.is_under(f.dir):
235 # They specified a source directory, but
236 # it's above the SConscript directory.
237 # Do the sensible thing and just use the
238 # SConcript directory.
239 ldir = fs.Dir(f.dir.get_path(sd))
240 try:
241 fs.chdir(ldir, change_os_dir=sconscript_chdir)
242 except OSError:
243 # There was no local directory, so we should be
244 # able to chdir to the Repository directory.
245 # Note that we do this directly, not through
246 # fs.chdir(), because we still need to
247 # interpret the stuff within the SConscript file
248 # relative to where we are logically.
249 fs.chdir(ldir, change_os_dir=False)
250 os.chdir(actual.dir.get_abspath())
252 # Append the SConscript directory to the beginning
253 # of sys.path so Python modules in the SConscript
254 # directory can be easily imported.
255 sys.path = [ f.dir.get_abspath() ] + sys.path
257 # This is the magic line that actually reads up
258 # and executes the stuff in the SConscript file.
259 # The locals for this frame contain the special
260 # bottom-of-the-stack marker so that any
261 # exceptions that occur when processing this
262 # SConscript can base the printed frames at this
263 # level and not show SCons internals as well.
264 call_stack[-1].globals.update({stack_bottom:1})
265 old_file = call_stack[-1].globals.get('__file__')
266 try:
267 del call_stack[-1].globals['__file__']
268 except KeyError:
269 pass
270 try:
271 try:
272 if Main.print_time:
273 start_time = time.perf_counter()
274 scriptdata = _file_.read()
275 scriptname = _file_.name
276 _file_.close()
277 if SCons.Debug.sconscript_trace:
278 print("scons: Entering "+str(scriptname))
279 exec(compile(scriptdata, scriptname, 'exec'), call_stack[-1].globals)
280 if SCons.Debug.sconscript_trace:
281 print("scons: Exiting "+str(scriptname))
282 except SConscriptReturn:
283 if SCons.Debug.sconscript_trace:
284 print("scons: Exiting "+str(scriptname))
285 else:
286 pass
287 finally:
288 if Main.print_time:
289 elapsed = time.perf_counter() - start_time
290 print('SConscript:%s took %0.3f ms' % (f.get_abspath(), elapsed * 1000.0))
292 if old_file is not None:
293 call_stack[-1].globals.update({__file__:old_file})
295 else:
296 handle_missing_SConscript(f, kw.get('must_exist', True))
298 finally:
299 SCons.Script.sconscript_reading = SCons.Script.sconscript_reading - 1
300 sys.path = old_sys_path
301 frame = call_stack.pop()
302 try:
303 fs.chdir(frame.prev_dir, change_os_dir=sconscript_chdir)
304 except OSError:
305 # There was no local directory, so chdir to the
306 # Repository directory. Like above, we do this
307 # directly.
308 fs.chdir(frame.prev_dir, change_os_dir=False)
309 rdir = frame.prev_dir.rdir()
310 rdir._create() # Make sure there's a directory there.
311 try:
312 os.chdir(rdir.get_abspath())
313 except OSError as e:
314 # We still couldn't chdir there, so raise the error,
315 # but only if actions are being executed.
317 # If the -n option was used, the directory would *not*
318 # have been created and we should just carry on and
319 # let things muddle through. This isn't guaranteed
320 # to work if the SConscript files are reading things
321 # from disk (for example), but it should work well
322 # enough for most configurations.
323 if SCons.Action.execute_actions:
324 raise e
326 results.append(frame.retval)
328 # if we only have one script, don't return a tuple
329 if len(results) == 1:
330 return results[0]
331 else:
332 return tuple(results)
334 def SConscript_exception(file=sys.stderr) -> None:
335 """Print an exception stack trace just for the SConscript file(s).
336 This will show users who have Python errors where the problem is,
337 without cluttering the output with all of the internal calls leading
338 up to where we exec the SConscript."""
339 exc_type, exc_value, exc_tb = sys.exc_info()
340 tb = exc_tb
341 while tb and stack_bottom not in tb.tb_frame.f_locals:
342 tb = tb.tb_next
343 if not tb:
344 # We did not find our exec statement, so this was actually a bug
345 # in SCons itself. Show the whole stack.
346 tb = exc_tb
347 stack = traceback.extract_tb(tb)
348 try:
349 type = exc_type.__name__
350 except AttributeError:
351 type = str(exc_type)
352 if type[:11] == "exceptions.":
353 type = type[11:]
354 file.write('%s: %s:\n' % (type, exc_value))
355 for fname, line, func, text in stack:
356 file.write(' File "%s", line %d:\n' % (fname, line))
357 file.write(' %s\n' % text)
359 def annotate(node):
360 """Annotate a node with the stack frame describing the
361 SConscript file and line number that created it."""
362 tb = sys.exc_info()[2]
363 while tb and stack_bottom not in tb.tb_frame.f_locals:
364 tb = tb.tb_next
365 if not tb:
366 # We did not find any exec of an SConscript file: what?!
367 raise SCons.Errors.InternalError("could not find SConscript stack frame")
368 node.creator = traceback.extract_stack(tb)[0]
370 # The following line would cause each Node to be annotated using the
371 # above function. Unfortunately, this is a *huge* performance hit, so
372 # leave this disabled until we find a more efficient mechanism.
373 #SCons.Node.Annotate = annotate
375 class SConsEnvironment(SCons.Environment.Base):
376 """An Environment subclass that contains all of the methods that
377 are particular to the wrapper SCons interface and which aren't
378 (or shouldn't be) part of the build engine itself.
380 Note that not all of the methods of this class have corresponding
381 global functions, there are some private methods.
385 # Private methods of an SConsEnvironment.
387 @staticmethod
388 def _get_major_minor_revision(version_string):
389 """Split a version string into major, minor and (optionally)
390 revision parts.
392 This is complicated by the fact that a version string can be
393 something like 3.2b1."""
394 version = version_string.split(' ')[0].split('.')
395 v_major = int(version[0])
396 v_minor = int(re.match(r'\d+', version[1]).group())
397 if len(version) >= 3:
398 v_revision = int(re.match(r'\d+', version[2]).group())
399 else:
400 v_revision = 0
401 return v_major, v_minor, v_revision
403 def _get_SConscript_filenames(self, ls, kw):
405 Convert the parameters passed to SConscript() calls into a list
406 of files and export variables. If the parameters are invalid,
407 throws SCons.Errors.UserError. Returns a tuple (l, e) where l
408 is a list of SConscript filenames and e is a list of exports.
410 exports = []
412 if len(ls) == 0:
413 try:
414 dirs = kw["dirs"]
415 except KeyError:
416 raise SCons.Errors.UserError("Invalid SConscript usage - no parameters")
418 if not is_List(dirs):
419 dirs = [ dirs ]
420 dirs = list(map(str, dirs))
422 name = kw.get('name', 'SConscript')
424 files = [os.path.join(n, name) for n in dirs]
426 elif len(ls) == 1:
428 files = ls[0]
430 elif len(ls) == 2:
432 files = ls[0]
433 exports = self.Split(ls[1])
435 else:
437 raise SCons.Errors.UserError("Invalid SConscript() usage - too many arguments")
439 if not is_List(files):
440 files = [ files ]
442 if kw.get('exports'):
443 exports.extend(self.Split(kw['exports']))
445 variant_dir = kw.get('variant_dir')
446 if variant_dir:
447 if len(files) != 1:
448 raise SCons.Errors.UserError("Invalid SConscript() usage - can only specify one SConscript with a variant_dir")
449 duplicate = kw.get('duplicate', 1)
450 src_dir = kw.get('src_dir')
451 if not src_dir:
452 src_dir, fname = os.path.split(str(files[0]))
453 files = [os.path.join(str(variant_dir), fname)]
454 else:
455 if not isinstance(src_dir, SCons.Node.Node):
456 src_dir = self.fs.Dir(src_dir)
457 fn = files[0]
458 if not isinstance(fn, SCons.Node.Node):
459 fn = self.fs.File(fn)
460 if fn.is_under(src_dir):
461 # Get path relative to the source directory.
462 fname = fn.get_path(src_dir)
463 files = [os.path.join(str(variant_dir), fname)]
464 else:
465 files = [fn.get_abspath()]
466 kw['src_dir'] = variant_dir
467 self.fs.VariantDir(variant_dir, src_dir, duplicate)
469 return (files, exports)
472 # Public methods of an SConsEnvironment. These get
473 # entry points in the global namespace so they can be called
474 # as global functions.
477 def Configure(self, *args, **kw):
478 if not SCons.Script.sconscript_reading:
479 raise SCons.Errors.UserError("Calling Configure from Builders is not supported.")
480 kw['_depth'] = kw.get('_depth', 0) + 1
481 return SCons.Environment.Base.Configure(self, *args, **kw)
483 def Default(self, *targets) -> None:
484 SCons.Script._Set_Default_Targets(self, targets)
486 @staticmethod
487 def EnsureSConsVersion(major, minor, revision: int=0) -> None:
488 """Exit abnormally if the SCons version is not late enough."""
489 # split string to avoid replacement during build process
490 if SCons.__version__ == '__' + 'VERSION__':
491 SCons.Warnings.warn(SCons.Warnings.DevelopmentVersionWarning,
492 "EnsureSConsVersion is ignored for development version")
493 return
494 scons_ver = SConsEnvironment._get_major_minor_revision(SCons.__version__)
495 if scons_ver < (major, minor, revision):
496 if revision:
497 scons_ver_string = '%d.%d.%d' % (major, minor, revision)
498 else:
499 scons_ver_string = '%d.%d' % (major, minor)
500 print("SCons %s or greater required, but you have SCons %s" % \
501 (scons_ver_string, SCons.__version__))
502 sys.exit(2)
504 @staticmethod
505 def EnsurePythonVersion(major, minor) -> None:
506 """Exit abnormally if the Python version is not late enough."""
507 if sys.version_info < (major, minor):
508 v = sys.version.split()[0]
509 print("Python %d.%d or greater required, but you have Python %s" %(major,minor,v))
510 sys.exit(2)
512 @staticmethod
513 def Exit(value: int=0) -> None:
514 sys.exit(value)
516 def Export(self, *vars, **kw) -> None:
517 for var in vars:
518 global_exports.update(compute_exports(self.Split(var)))
519 global_exports.update(kw)
521 @staticmethod
522 def GetLaunchDir():
523 global launch_dir
524 return launch_dir
526 def GetOption(self, name):
527 name = self.subst(name)
528 return SCons.Script.Main.GetOption(name)
530 def Help(self, text, append: bool = False, keep_local: bool = False) -> None:
531 """Update the help text.
533 The previous help text has *text* appended to it, except on the
534 first call. On first call, the values of *append* and *keep_local*
535 are considered to determine what is appended to.
537 Arguments:
538 text: string to add to the help text.
539 append: on first call, if true, keep the existing help text
540 (default False).
541 keep_local: on first call, if true and *append* is also true,
542 keep only the help text from AddOption calls.
544 .. versionchanged:: 4.6.0
545 The *keep_local* parameter was added.
547 text = self.subst(text, raw=1)
548 SCons.Script.HelpFunction(text, append=append, keep_local=keep_local)
550 def Import(self, *vars):
551 try:
552 frame = call_stack[-1]
553 globals = frame.globals
554 exports = frame.exports
555 for var in vars:
556 var = self.Split(var)
557 for v in var:
558 if v == '*':
559 globals.update(global_exports)
560 globals.update(exports)
561 else:
562 if v in exports:
563 globals[v] = exports[v]
564 else:
565 globals[v] = global_exports[v]
566 except KeyError as x:
567 raise SCons.Errors.UserError("Import of non-existent variable '%s'"%x)
569 def SConscript(self, *ls, **kw):
570 """Execute SCons configuration files.
572 Parameters:
573 *ls (str or list): configuration file(s) to execute.
575 Keyword arguments:
576 dirs (list): execute SConscript in each listed directory.
577 name (str): execute script 'name' (used only with 'dirs').
578 exports (list or dict): locally export variables the
579 called script(s) can import.
580 variant_dir (str): mirror sources needed for the build in
581 a variant directory to allow building in it.
582 duplicate (bool): physically duplicate sources instead of just
583 adjusting paths of derived files (used only with 'variant_dir')
584 (default is True).
585 must_exist (bool): fail if a requested script is missing
586 (default is False, default is deprecated).
588 Returns:
589 list of variables returned by the called script
591 Raises:
592 UserError: a script is not found and such exceptions are enabled.
595 def subst_element(x, subst=self.subst):
596 if SCons.Util.is_List(x):
597 x = list(map(subst, x))
598 else:
599 x = subst(x)
600 return x
601 ls = list(map(subst_element, ls))
602 subst_kw = {}
603 for key, val in kw.items():
604 if is_String(val):
605 val = self.subst(val)
606 elif SCons.Util.is_List(val):
607 val = [self.subst(v) if is_String(v) else v for v in val]
608 subst_kw[key] = val
610 files, exports = self._get_SConscript_filenames(ls, subst_kw)
611 subst_kw['exports'] = exports
612 return _SConscript(self.fs, *files, **subst_kw)
614 @staticmethod
615 def SConscriptChdir(flag: bool) -> None:
616 global sconscript_chdir
617 sconscript_chdir = flag
619 def SetOption(self, name, value) -> None:
620 name = self.subst(name)
621 SCons.Script.Main.SetOption(name, value)
626 SCons.Environment.Environment = SConsEnvironment
628 def Configure(*args, **kw):
629 if not SCons.Script.sconscript_reading:
630 raise SCons.Errors.UserError("Calling Configure from Builders is not supported.")
631 kw['_depth'] = 1
632 return SCons.SConf.SConf(*args, **kw)
634 # It's very important that the DefaultEnvironmentCall() class stay in this
635 # file, with the get_calling_namespaces() function, the compute_exports()
636 # function, the Frame class and the SConsEnvironment.Export() method.
637 # These things make up the calling stack leading up to the actual global
638 # Export() or SConscript() call that the user issued. We want to allow
639 # users to export local variables that they define, like so:
641 # def func():
642 # x = 1
643 # Export('x')
645 # To support this, the get_calling_namespaces() function assumes that
646 # the *first* stack frame that's not from this file is the local frame
647 # for the Export() or SConscript() call.
649 _DefaultEnvironmentProxy = None
651 def get_DefaultEnvironmentProxy():
652 global _DefaultEnvironmentProxy
653 if not _DefaultEnvironmentProxy:
654 default_env = SCons.Defaults.DefaultEnvironment()
655 _DefaultEnvironmentProxy = SCons.Environment.NoSubstitutionProxy(default_env)
656 return _DefaultEnvironmentProxy
658 class DefaultEnvironmentCall:
659 """A class that implements "global function" calls of
660 Environment methods by fetching the specified method from the
661 DefaultEnvironment's class. Note that this uses an intermediate
662 proxy class instead of calling the DefaultEnvironment method
663 directly so that the proxy can override the subst() method and
664 thereby prevent expansion of construction variables (since from
665 the user's point of view this was called as a global function,
666 with no associated construction environment)."""
667 def __init__(self, method_name, subst: int=0) -> None:
668 self.method_name = method_name
669 if subst:
670 self.factory = SCons.Defaults.DefaultEnvironment
671 else:
672 self.factory = get_DefaultEnvironmentProxy
673 def __call__(self, *args, **kw):
674 env = self.factory()
675 method = getattr(env, self.method_name)
676 return method(*args, **kw)
679 def BuildDefaultGlobals():
681 Create a dictionary containing all the default globals for
682 SConstruct and SConscript files.
685 global GlobalDict
686 if GlobalDict is None:
687 GlobalDict = {}
689 import SCons.Script
690 d = SCons.Script.__dict__
691 def not_a_module(m, d=d, mtype=type(SCons.Script)) -> bool:
692 return not isinstance(d[m], mtype)
693 for m in filter(not_a_module, dir(SCons.Script)):
694 GlobalDict[m] = d[m]
696 return GlobalDict.copy()
698 # Local Variables:
699 # tab-width:4
700 # indent-tabs-mode:nil
701 # End:
702 # vim: set expandtab tabstop=4 shiftwidth=4: