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."""
30 import SCons
.Environment
33 import SCons
.Node
.Alias
38 from SCons
.Util
import is_List
, is_String
, is_Dict
, flatten
39 from SCons
.Node
import SConscriptNodes
49 class SConscriptReturn(Exception):
52 launch_dir
= os
.path
.abspath(os
.curdir
)
56 # global exports set by Export():
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."""
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__
:
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()
92 for export
in exports
:
97 retval
[export
] = loc
[export
]
99 retval
[export
] = glob
[export
]
100 except KeyError as x
:
101 raise SCons
.Errors
.UserError("Export of non-existent variable '%s'"%x)
106 """A frame on the SConstruct/SConscript call stack"""
107 def __init__(self
, fs
, exports
, sconscript
) -> None:
108 self
.globals = BuildDefaultGlobals()
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
118 self
.sconscript
= fs
.File(str(sconscript
))
120 # the SConstruct/SConscript call stack:
123 # For documentation on the methods in this file, see the scons man-page
125 def Return(*vars, **kw
):
128 fvars
= flatten(vars)
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)
136 call_stack
[-1].retval
= retval
[0]
138 call_stack
[-1].retval
= tuple(retval
)
140 stop
= kw
.get('stop', True)
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
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.
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
169 if not SCons
.Script
._no
_missing
_sconscript
: # system default changed: ok
171 msg
= f
"missing SConscript file {f.get_internal_path()!r}"
172 raise SCons
.Errors
.UserError(msg
)
175 def _SConscript(fs
, *files
, **kw
):
177 sd
= fs
.SConstruct_dir
.rdir()
178 exports
= kw
.get('exports', [])
180 # evaluate each SConscript file
183 call_stack
.append(Frame(fs
, exports
, fn
))
184 old_sys_path
= sys
.path
186 SCons
.Script
.sconscript_reading
= SCons
.Script
.sconscript_reading
+ 1
188 exec(sys
.stdin
.read(), call_stack
[-1].globals)
190 if isinstance(fn
, SCons
.Node
.Node
):
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)
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.
216 _file_
= open(f
.get_abspath(), "rb")
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.
229 src_dir
= kw
['src_dir']
231 ldir
= fs
.Dir(f
.dir.get_path(sd
))
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
))
241 fs
.chdir(ldir
, change_os_dir
=sconscript_chdir
)
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__')
267 del call_stack
[-1].globals['__file__']
273 start_time
= time
.perf_counter()
274 scriptdata
= _file_
.read()
275 scriptname
= _file_
.name
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
))
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
})
296 handle_missing_SConscript(f
, kw
.get('must_exist', True))
299 SCons
.Script
.sconscript_reading
= SCons
.Script
.sconscript_reading
- 1
300 sys
.path
= old_sys_path
301 frame
= call_stack
.pop()
303 fs
.chdir(frame
.prev_dir
, change_os_dir
=sconscript_chdir
)
305 # There was no local directory, so chdir to the
306 # Repository directory. Like above, we do this
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.
312 os
.chdir(rdir
.get_abspath())
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
:
326 results
.append(frame
.retval
)
328 # if we only have one script, don't return a tuple
329 if len(results
) == 1:
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()
341 while tb
and stack_bottom
not in tb
.tb_frame
.f_locals
:
344 # We did not find our exec statement, so this was actually a bug
345 # in SCons itself. Show the whole stack.
347 stack
= traceback
.extract_tb(tb
)
349 type = exc_type
.__name
__
350 except AttributeError:
352 if type[:11] == "exceptions.":
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
)
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
:
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.
388 def _get_major_minor_revision(version_string
):
389 """Split a version string into major, minor and (optionally)
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())
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.
416 raise SCons
.Errors
.UserError("Invalid SConscript usage - no parameters")
418 if not is_List(dirs
):
420 dirs
= list(map(str, dirs
))
422 name
= kw
.get('name', 'SConscript')
424 files
= [os
.path
.join(n
, name
) for n
in dirs
]
433 exports
= self
.Split(ls
[1])
437 raise SCons
.Errors
.UserError("Invalid SConscript() usage - too many arguments")
439 if not is_List(files
):
442 if kw
.get('exports'):
443 exports
.extend(self
.Split(kw
['exports']))
445 variant_dir
= kw
.get('variant_dir')
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')
452 src_dir
, fname
= os
.path
.split(str(files
[0]))
453 files
= [os
.path
.join(str(variant_dir
), fname
)]
455 if not isinstance(src_dir
, SCons
.Node
.Node
):
456 src_dir
= self
.fs
.Dir(src_dir
)
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
)]
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
)
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")
494 scons_ver
= SConsEnvironment
._get
_major
_minor
_revision
(SCons
.__version
__)
495 if scons_ver
< (major
, minor
, revision
):
497 scons_ver_string
= '%d.%d.%d' % (major
, minor
, revision
)
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
__))
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
))
513 def Exit(value
: int=0) -> None:
516 def Export(self
, *vars, **kw
) -> None:
518 global_exports
.update(compute_exports(self
.Split(var
)))
519 global_exports
.update(kw
)
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.
538 text: string to add to the help text.
539 append: on first call, if true, keep the existing help text
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):
552 frame
= call_stack
[-1]
553 globals = frame
.globals
554 exports
= frame
.exports
556 var
= self
.Split(var
)
559 globals.update(global_exports
)
560 globals.update(exports
)
563 globals[v
] = exports
[v
]
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.
573 *ls (str or list): configuration file(s) to execute.
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')
585 must_exist (bool): fail if a requested script is missing
586 (default is False, default is deprecated).
589 list of variables returned by the called script
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
))
601 ls
= list(map(subst_element
, ls
))
603 for key
, val
in kw
.items():
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
]
610 files
, exports
= self
._get
_SConscript
_filenames
(ls
, subst_kw
)
611 subst_kw
['exports'] = exports
612 return _SConscript(self
.fs
, *files
, **subst_kw
)
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.")
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:
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
670 self
.factory
= SCons
.Defaults
.DefaultEnvironment
672 self
.factory
= get_DefaultEnvironmentProxy
673 def __call__(self
, *args
, **kw
):
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.
686 if GlobalDict
is None:
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
)):
696 return GlobalDict
.copy()
700 # indent-tabs-mode:nil
702 # vim: set expandtab tabstop=4 shiftwidth=4: