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