1 """SCons.Script.SConscript
3 This module defines the Python API provided to SConscript and SConstruct
11 # Permission is hereby granted, free of charge, to any person obtaining
12 # a copy of this software and associated documentation files (the
13 # "Software"), to deal in the Software without restriction, including
14 # without limitation the rights to use, copy, modify, merge, publish,
15 # distribute, sublicense, and/or sell copies of the Software, and to
16 # permit persons to whom the Software is furnished to do so, subject to
17 # the following conditions:
19 # The above copyright notice and this permission notice shall be included
20 # in all copies or substantial portions of the Software.
22 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
23 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
24 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37 import SCons.Environment
40 import SCons.Node.Alias
45 import SCons.Script.Main
58 # The following variables used to live in this module. Some
59 # SConscript files out there may have referred to them directly as
60 # SCons.Script.SConscript.*. This is now supported by some special
61 # handling towards the bottom of the SConscript.__init__.py module.
64 #BuildTargets = TargetList()
65 #CommandLineTargets = []
68 launch_dir = os.path.abspath(os.curdir)
72 # global exports set by Export():
78 def get_calling_namespaces():
79 """Return the locals and globals for the function that called
80 into this module in the current call stack."""
82 except ZeroDivisionError: frame = sys.exc_info()[2].tb_frame
84 # Find the first frame that *isn't* from this file. This means
85 # that we expect all of the SCons frames that implement an Export()
86 # or SConscript() call to be in this file, so that we can identify
87 # the first non-Script.SConscript frame as the user's local calling
88 # environment, and the locals and globals dictionaries from that
89 # frame as the calling namespaces. See the comment below preceding
90 # the DefaultEnvironmentCall block for even more explanation.
91 while frame.f_globals.get("__name__") == __name__:
94 return frame.f_locals, frame.f_globals
97 def compute_exports(exports):
98 """Compute a dictionary of exports given one of the parameters
99 to the Export() function or the exports argument to SConscript()."""
101 loc, glob = get_calling_namespaces()
105 for export in exports:
106 if SCons.Util.is_Dict(export):
107 retval.update(export)
110 retval[export] = loc[export]
112 retval[export] = glob[export]
114 raise SCons.Errors.UserError, "Export of non-existent variable '%s'"%x
119 """A frame on the SConstruct/SConscript call stack"""
120 def __init__(self, fs, exports, sconscript):
121 self.globals = BuildDefaultGlobals()
123 self.prev_dir = fs.getcwd()
124 self.exports = compute_exports(exports) # exports from the calling SConscript
125 # make sure the sconscript attr is a Node.
126 if isinstance(sconscript, SCons.Node.Node):
127 self.sconscript = sconscript
129 self.sconscript = fs.File(str(sconscript))
131 # the SConstruct/SConscript call stack:
134 # For documentation on the methods in this file, see the scons man-page
140 for v in string.split(var):
141 retval.append(call_stack[-1].globals[v])
143 raise SCons.Errors.UserError, "Return of non-existent variable '%s'"%x
146 call_stack[-1].retval = retval[0]
148 call_stack[-1].retval = tuple(retval)
151 stack_bottom = '% Stack boTTom %' # hard to define a variable w/this name :)
153 def _SConscript(fs, *files, **kw):
155 sd = fs.SConstruct_dir.rdir()
156 exports = kw.get('exports', [])
158 # evaluate each SConscript file
161 call_stack.append(Frame(fs, exports, fn))
162 old_sys_path = sys.path
164 SCons.Script.sconscript_reading = SCons.Script.sconscript_reading + 1
166 exec sys.stdin in call_stack[-1].globals
168 if isinstance(fn, SCons.Node.Node):
174 # Change directory to the top of the source
175 # tree to make sure the os's cwd and the cwd of
176 # fs match so we can open the SConscript.
177 fs.chdir(top, change_os_dir=1)
179 _file_ = open(f.rstr(), "r")
180 elif f.has_src_builder():
181 # The SConscript file apparently exists in a source
182 # code management system. Build it, but then clear
183 # the builder so that it doesn't get built *again*
184 # during the actual build phase.
188 _file_ = open(str(f), "r")
190 # Chdir to the SConscript directory. Use a path
191 # name relative to the SConstruct file so that if
192 # we're using the -f option, we're essentially
193 # creating a parallel SConscript directory structure
194 # in our local directory tree.
196 # XXX This is broken for multiple-repository cases
197 # where the SConstruct and SConscript files might be
198 # in different Repositories. For now, cross that
199 # bridge when someone comes to it.
200 ldir = fs.Dir(f.dir.get_path(sd))
202 fs.chdir(ldir, change_os_dir=sconscript_chdir)
204 # There was no local directory, so we should be
205 # able to chdir to the Repository directory.
206 # Note that we do this directly, not through
207 # fs.chdir(), because we still need to
208 # interpret the stuff within the SConscript file
209 # relative to where we are logically.
210 fs.chdir(ldir, change_os_dir=0)
211 os.chdir(f.rfile().dir.get_abspath())
213 # Append the SConscript directory to the beginning
214 # of sys.path so Python modules in the SConscript
215 # directory can be easily imported.
216 sys.path = [ f.dir.get_abspath() ] + sys.path
218 # This is the magic line that actually reads up
219 # and executes the stuff in the SConscript file.
220 # The locals for this frame contain the special
221 # bottom-of-the-stack marker so that any
222 # exceptions that occur when processing this
223 # SConscript can base the printed frames at this
224 # level and not show SCons internals as well.
225 call_stack[-1].globals.update({stack_bottom:1})
226 exec _file_ in call_stack[-1].globals
228 SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning,
229 "Ignoring missing SConscript '%s'" % f.path)
232 SCons.Script.sconscript_reading = SCons.Script.sconscript_reading - 1
233 sys.path = old_sys_path
234 frame = call_stack.pop()
236 fs.chdir(frame.prev_dir, change_os_dir=sconscript_chdir)
238 # There was no local directory, so chdir to the
239 # Repository directory. Like above, we do this
241 fs.chdir(frame.prev_dir, change_os_dir=0)
242 rdir = frame.prev_dir.rdir()
243 rdir._create() # Make sure there's a directory there.
244 os.chdir(rdir.get_abspath())
246 results.append(frame.retval)
248 # if we only have one script, don't return a tuple
249 if len(results) == 1:
252 return tuple(results)
254 def SConscript_exception(file=sys.stderr):
255 """Print an exception stack trace just for the SConscript file(s).
256 This will show users who have Python errors where the problem is,
257 without cluttering the output with all of the internal calls leading
258 up to where we exec the SConscript."""
259 exc_type, exc_value, exc_tb = sys.exc_info()
261 while tb and not tb.tb_frame.f_locals.has_key(stack_bottom):
264 # We did not find our exec statement, so this was actually a bug
265 # in SCons itself. Show the whole stack.
267 stack = traceback.extract_tb(tb)
269 if type[:11] == "exceptions.":
271 file.write('%s: %s:\n' % (type, exc_value))
272 for fname, line, func, text in stack:
273 file.write(' File "%s", line %d:\n' % (fname, line))
274 file.write(' %s\n' % text)
277 """Annotate a node with the stack frame describing the
278 SConscript file and line number that created it."""
279 tb = exc_tb = sys.exc_info()[2]
280 while tb and not tb.tb_frame.f_locals.has_key(stack_bottom):
283 # We did not find any exec of an SConscript file: what?!
284 raise InternalError, "could not find SConscript stack frame"
285 node.creator = traceback.extract_stack(tb)[0]
287 # The following line would cause each Node to be annotated using the
288 # above function. Unfortunately, this is a *huge* performance hit, so
289 # leave this disabled until we find a more efficient mechanism.
290 #SCons.Node.Annotate = annotate
292 class SConsEnvironment(SCons.Environment.Base):
293 """An Environment subclass that contains all of the methods that
294 are particular to the wrapper SCons interface and which aren't
295 (or shouldn't be) part of the build engine itself.
297 Note that not all of the methods of this class have corresponding
298 global functions, there are some private methods.
302 # Private methods of an SConsEnvironment.
304 def _exceeds_version(self, major, minor, v_major, v_minor):
305 """Return 1 if 'major' and 'minor' are greater than the version
306 in 'v_major' and 'v_minor', and 0 otherwise."""
307 return (major > v_major or (major == v_major and minor > v_minor))
309 def _get_major_minor_revision(self, version_string):
310 """Split a version string into major, minor and (optionally)
313 This is complicated by the fact that a version string can be
314 something like 3.2b1."""
315 version = string.split(string.split(version_string, ' ')[0], '.')
316 v_major = int(version[0])
317 v_minor = int(re.match('\d+', version[1]).group())
318 if len(version) >= 3:
319 v_revision = int(re.match('\d+', version[2]).group())
322 return v_major, v_minor, v_revision
324 def _get_SConscript_filenames(self, ls, kw):
326 Convert the parameters passed to # SConscript() calls into a list
327 of files and export variables. If the parameters are invalid,
328 throws SCons.Errors.UserError. Returns a tuple (l, e) where l
329 is a list of SConscript filenames and e is a list of exports.
337 raise SCons.Errors.UserError, \
338 "Invalid SConscript usage - no parameters"
340 if not SCons.Util.is_List(dirs):
342 dirs = map(str, dirs)
344 name = kw.get('name', 'SConscript')
346 files = map(lambda n, name = name: os.path.join(n, name), dirs)
355 exports = self.Split(ls[1])
359 raise SCons.Errors.UserError, \
360 "Invalid SConscript() usage - too many arguments"
362 if not SCons.Util.is_List(files):
365 if kw.get('exports'):
366 exports.extend(self.Split(kw['exports']))
368 build_dir = kw.get('build_dir')
371 raise SCons.Errors.UserError, \
372 "Invalid SConscript() usage - can only specify one SConscript with a build_dir"
373 duplicate = kw.get('duplicate', 1)
374 src_dir = kw.get('src_dir')
376 src_dir, fname = os.path.split(str(files[0]))
378 if not isinstance(src_dir, SCons.Node.Node):
379 src_dir = self.fs.Dir(src_dir)
381 if not isinstance(fn, SCons.Node.Node):
382 fn = self.fs.File(fn)
383 if fn.is_under(src_dir):
384 # Get path relative to the source directory.
385 fname = fn.get_path(src_dir)
387 # Fast way to only get the terminal path component of a Node.
388 fname = fn.get_path(fn.dir)
389 self.fs.BuildDir(build_dir, src_dir, duplicate)
390 files = [os.path.join(str(build_dir), fname)]
392 return (files, exports)
395 # Public methods of an SConsEnvironment. These get
396 # entry points in the global name space so they can be called
397 # as global functions.
400 def Configure(self, *args, **kw):
401 if not SCons.Script.sconscript_reading:
402 raise SCons.Errors.UserError, "Calling Configure from Builders is not supported."
403 kw['_depth'] = kw.get('_depth', 0) + 1
404 return apply(SCons.Environment.Base.Configure, (self,)+args, kw)
406 def Default(self, *targets):
407 SCons.Script._Set_Default_Targets(self, targets)
409 def EnsureSConsVersion(self, major, minor, revision=0):
410 """Exit abnormally if the SCons version is not late enough."""
411 scons_ver = self._get_major_minor_revision(SCons.__version__)
412 if scons_ver < (major, minor, revision):
414 scons_ver_string = '%d.%d.%d' % (major, minor, revision)
416 scons_ver_string = '%d.%d' % (major, minor)
417 print "SCons %s or greater required, but you have SCons %s" % \
418 (scons_ver_string, SCons.__version__)
421 def EnsurePythonVersion(self, major, minor):
422 """Exit abnormally if the Python version is not late enough."""
424 v_major, v_minor, v_micro, release, serial = sys.version_info
425 python_ver = (v_major, v_minor)
426 except AttributeError:
427 python_ver = self._get_major_minor_revision(sys.version)[:2]
428 if python_ver < (major, minor):
429 v = string.split(sys.version, " ", 1)[0]
430 print "Python %d.%d or greater required, but you have Python %s" %(major,minor,v)
433 def Exit(self, value=0):
436 def Export(self, *vars):
438 global_exports.update(compute_exports(self.Split(var)))
440 def GetLaunchDir(self):
444 def GetOption(self, name):
445 name = self.subst(name)
446 return SCons.Script.Main.ssoptions.get(name)
448 def Help(self, text):
449 text = self.subst(text, raw=1)
450 SCons.Script.HelpFunction(text)
452 def Import(self, *vars):
454 frame = call_stack[-1]
455 globals = frame.globals
456 exports = frame.exports
458 var = self.Split(var)
461 globals.update(global_exports)
462 globals.update(exports)
464 if exports.has_key(v):
465 globals[v] = exports[v]
467 globals[v] = global_exports[v]
469 raise SCons.Errors.UserError, "Import of non-existent variable '%s'"%x
471 def SConscript(self, *ls, **kw):
472 def subst_element(x, subst=self.subst):
473 if SCons.Util.is_List(x):
478 ls = map(subst_element, ls)
480 for key, val in kw.items():
481 if SCons.Util.is_String(val):
482 val = self.subst(val)
483 elif SCons.Util.is_List(val):
486 if SCons.Util.is_String(v):
492 files, exports = self._get_SConscript_filenames(ls, subst_kw)
494 return apply(_SConscript, [self.fs,] + files, {'exports' : exports})
496 def SConscriptChdir(self, flag):
497 global sconscript_chdir
498 sconscript_chdir = flag
500 def SetOption(self, name, value):
501 name = self.subst(name)
502 SCons.Script.Main.ssoptions.set(name, value)
507 SCons.Environment.Environment = SConsEnvironment
509 def Configure(*args, **kw):
510 if not SCons.Script.sconscript_reading:
511 raise SCons.Errors.UserError, "Calling Configure from Builders is not supported."
513 return apply(SCons.SConf.SConf, args, kw)
515 # It's very important that the DefaultEnvironmentCall() class stay in this
516 # file, with the get_calling_namespaces() function, the compute_exports()
517 # function, the Frame class and the SConsEnvironment.Export() method.
518 # These things make up the calling stack leading up to the actual global
519 # Export() or SConscript() call that the user issued. We want to allow
520 # users to export local variables that they define, like so:
526 # To support this, the get_calling_namespaces() function assumes that
527 # the *first* stack frame that's not from this file is the local frame
528 # for the Export() or SConscript() call.
530 _DefaultEnvironmentProxy = None
532 def get_DefaultEnvironmentProxy():
533 global _DefaultEnvironmentProxy
534 if not _DefaultEnvironmentProxy:
535 default_env = SCons.Defaults.DefaultEnvironment()
536 _DefaultEnvironmentProxy = SCons.Environment.NoSubstitutionProxy(default_env)
537 return _DefaultEnvironmentProxy
539 class DefaultEnvironmentCall:
540 """A class that implements "global function" calls of
541 Environment methods by fetching the specified method from the
542 DefaultEnvironment's class. Note that this uses an intermediate
543 proxy class instead of calling the DefaultEnvironment method
544 directly so that the proxy can override the subst() method and
545 thereby prevent expansion of construction variables (since from
546 the user's point of view this was called as a global function,
547 with no associated construction environment)."""
548 def __init__(self, method_name, subst=0):
549 self.method_name = method_name
551 self.factory = SCons.Defaults.DefaultEnvironment
553 self.factory = get_DefaultEnvironmentProxy
554 def __call__(self, *args, **kw):
556 method = getattr(env, self.method_name)
557 return apply(method, args, kw)
560 def BuildDefaultGlobals():
562 Create a dictionary containing all the default globals for
563 SConstruct and SConscript files.
567 if GlobalDict is None:
571 d = SCons.Script.__dict__
572 def not_a_module(m, d=d, mtype=type(SCons.Script)):
573 return type(d[m]) != mtype
574 for m in filter(not_a_module, dir(SCons.Script)):
577 return GlobalDict.copy()