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