f73161fde071b45bebaae7254187727774ea63e7
[scons.git] / src / engine / SCons / Node / FS.py
1 """scons.Node.FS
2
3 File system nodes.
4
5 These Nodes represent the canonical external objects that people think
6 of when they think of building software: files and directories.
7
8 This holds a "default_fs" variable that should be initialized with an FS
9 that can be used by scripts or modules looking for the canonical default.
10
11 """
12
13 #
14 # __COPYRIGHT__
15 #
16 # Permission is hereby granted, free of charge, to any person obtaining
17 # a copy of this software and associated documentation files (the
18 # "Software"), to deal in the Software without restriction, including
19 # without limitation the rights to use, copy, modify, merge, publish,
20 # distribute, sublicense, and/or sell copies of the Software, and to
21 # permit persons to whom the Software is furnished to do so, subject to
22 # the following conditions:
23 #
24 # The above copyright notice and this permission notice shall be included
25 # in all copies or substantial portions of the Software.
26 #
27 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
28 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
29 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 #
35
36 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37
38 import fnmatch
39 import os
40 import os.path
41 import re
42 import shutil
43 import stat
44 import string
45 import sys
46 import time
47 import cStringIO
48
49 import SCons.Action
50 from SCons.Debug import logInstanceCreation
51 import SCons.Errors
52 import SCons.Memoize
53 import SCons.Node
54 import SCons.Node.Alias
55 import SCons.Subst
56 import SCons.Util
57 import SCons.Warnings
58
59 from SCons.Debug import Trace
60
61 # The max_drift value:  by default, use a cached signature value for
62 # any file that's been untouched for more than two days.
63 default_max_drift = 2*24*60*60
64
65 #
66 # We stringify these file system Nodes a lot.  Turning a file system Node
67 # into a string is non-trivial, because the final string representation
68 # can depend on a lot of factors:  whether it's a derived target or not,
69 # whether it's linked to a repository or source directory, and whether
70 # there's duplication going on.  The normal technique for optimizing
71 # calculations like this is to memoize (cache) the string value, so you
72 # only have to do the calculation once.
73 #
74 # A number of the above factors, however, can be set after we've already
75 # been asked to return a string for a Node, because a Repository() or
76 # VariantDir() call or the like may not occur until later in SConscript
77 # files.  So this variable controls whether we bother trying to save
78 # string values for Nodes.  The wrapper interface can set this whenever
79 # they're done mucking with Repository and VariantDir and the other stuff,
80 # to let this module know it can start returning saved string values
81 # for Nodes.
82 #
83 Save_Strings = None
84
85 def save_strings(val):
86     global Save_Strings
87     Save_Strings = val
88
89 #
90 # Avoid unnecessary function calls by recording a Boolean value that
91 # tells us whether or not os.path.splitdrive() actually does anything
92 # on this system, and therefore whether we need to bother calling it
93 # when looking up path names in various methods below.
94
95
96 do_splitdrive = None
97
98 def initialize_do_splitdrive():
99     global do_splitdrive
100     drive, path = os.path.splitdrive('X:/foo')
101     do_splitdrive = not not drive
102
103 initialize_do_splitdrive()
104
105 #
106
107 needs_normpath_check = None
108
109 def initialize_normpath_check():
110     """
111     Initialize the normpath_check regular expression.
112
113     This function is used by the unit tests to re-initialize the pattern
114     when testing for behavior with different values of os.sep.
115     """
116     global needs_normpath_check
117     if os.sep == '/':
118         pattern = r'.*/|\.$|\.\.$'
119     else:
120         pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep)
121     needs_normpath_check = re.compile(pattern)
122
123 initialize_normpath_check()
124
125 #
126 # SCons.Action objects for interacting with the outside world.
127 #
128 # The Node.FS methods in this module should use these actions to
129 # create and/or remove files and directories; they should *not* use
130 # os.{link,symlink,unlink,mkdir}(), etc., directly.
131 #
132 # Using these SCons.Action objects ensures that descriptions of these
133 # external activities are properly displayed, that the displays are
134 # suppressed when the -s (silent) option is used, and (most importantly)
135 # the actions are disabled when the the -n option is used, in which case
136 # there should be *no* changes to the external file system(s)...
137 #
138
139 if hasattr(os, 'link'):
140     def _hardlink_func(fs, src, dst):
141         # If the source is a symlink, we can't just hard-link to it
142         # because a relative symlink may point somewhere completely
143         # different.  We must disambiguate the symlink and then
144         # hard-link the final destination file.
145         while fs.islink(src):
146             link = fs.readlink(src)
147             if not os.path.isabs(link):
148                 src = link
149             else:
150                 src = os.path.join(os.path.dirname(src), link)
151         fs.link(src, dst)
152 else:
153     _hardlink_func = None
154
155 if hasattr(os, 'symlink'):
156     def _softlink_func(fs, src, dst):
157         fs.symlink(src, dst)
158 else:
159     _softlink_func = None
160
161 def _copy_func(fs, src, dest):
162     shutil.copy2(src, dest)
163     st = fs.stat(src)
164     fs.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
165
166
167 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
168                     'hard-copy', 'soft-copy', 'copy']
169
170 Link_Funcs = [] # contains the callables of the specified duplication style
171
172 def set_duplicate(duplicate):
173     # Fill in the Link_Funcs list according to the argument
174     # (discarding those not available on the platform).
175
176     # Set up the dictionary that maps the argument names to the
177     # underlying implementations.  We do this inside this function,
178     # not in the top-level module code, so that we can remap os.link
179     # and os.symlink for testing purposes.
180     link_dict = {
181         'hard' : _hardlink_func,
182         'soft' : _softlink_func,
183         'copy' : _copy_func
184     }
185
186     if not duplicate in Valid_Duplicates:
187         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
188                                            "should be in Valid_Duplicates")
189     global Link_Funcs
190     Link_Funcs = []
191     for func in string.split(duplicate,'-'):
192         if link_dict[func]:
193             Link_Funcs.append(link_dict[func])
194
195 def LinkFunc(target, source, env):
196     # Relative paths cause problems with symbolic links, so
197     # we use absolute paths, which may be a problem for people
198     # who want to move their soft-linked src-trees around. Those
199     # people should use the 'hard-copy' mode, softlinks cannot be
200     # used for that; at least I have no idea how ...
201     src = source[0].abspath
202     dest = target[0].abspath
203     dir, file = os.path.split(dest)
204     if dir and not target[0].fs.isdir(dir):
205         os.makedirs(dir)
206     if not Link_Funcs:
207         # Set a default order of link functions.
208         set_duplicate('hard-soft-copy')
209     fs = source[0].fs
210     # Now link the files with the previously specified order.
211     for func in Link_Funcs:
212         try:
213             func(fs, src, dest)
214             break
215         except (IOError, OSError):
216             # An OSError indicates something happened like a permissions
217             # problem or an attempt to symlink across file-system
218             # boundaries.  An IOError indicates something like the file
219             # not existing.  In either case, keeping trying additional
220             # functions in the list and only raise an error if the last
221             # one failed.
222             if func == Link_Funcs[-1]:
223                 # exception of the last link method (copy) are fatal
224                 raise
225             else:
226                 pass
227     return 0
228
229 Link = SCons.Action.Action(LinkFunc, None)
230 def LocalString(target, source, env):
231     return 'Local copy of %s from %s' % (target[0], source[0])
232
233 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
234
235 def UnlinkFunc(target, source, env):
236     t = target[0]
237     t.fs.unlink(t.abspath)
238     return 0
239
240 Unlink = SCons.Action.Action(UnlinkFunc, None)
241
242 def MkdirFunc(target, source, env):
243     t = target[0]
244     if not t.exists():
245         t.fs.mkdir(t.abspath)
246     return 0
247
248 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
249
250 MkdirBuilder = None
251
252 def get_MkdirBuilder():
253     global MkdirBuilder
254     if MkdirBuilder is None:
255         import SCons.Builder
256         import SCons.Defaults
257         # "env" will get filled in by Executor.get_build_env()
258         # calling SCons.Defaults.DefaultEnvironment() when necessary.
259         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
260                                              env = None,
261                                              explain = None,
262                                              is_explicit = None,
263                                              target_scanner = SCons.Defaults.DirEntryScanner,
264                                              name = "MkdirBuilder")
265     return MkdirBuilder
266
267 class _Null:
268     pass
269
270 _null = _Null()
271
272 DefaultSCCSBuilder = None
273 DefaultRCSBuilder = None
274
275 def get_DefaultSCCSBuilder():
276     global DefaultSCCSBuilder
277     if DefaultSCCSBuilder is None:
278         import SCons.Builder
279         # "env" will get filled in by Executor.get_build_env()
280         # calling SCons.Defaults.DefaultEnvironment() when necessary.
281         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
282         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
283                                                    env = None,
284                                                    name = "DefaultSCCSBuilder")
285     return DefaultSCCSBuilder
286
287 def get_DefaultRCSBuilder():
288     global DefaultRCSBuilder
289     if DefaultRCSBuilder is None:
290         import SCons.Builder
291         # "env" will get filled in by Executor.get_build_env()
292         # calling SCons.Defaults.DefaultEnvironment() when necessary.
293         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
294         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
295                                                   env = None,
296                                                   name = "DefaultRCSBuilder")
297     return DefaultRCSBuilder
298
299 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
300 _is_cygwin = sys.platform == "cygwin"
301 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
302     def _my_normcase(x):
303         return x
304 else:
305     def _my_normcase(x):
306         return string.upper(x)
307
308
309
310 class DiskChecker:
311     def __init__(self, type, do, ignore):
312         self.type = type
313         self.do = do
314         self.ignore = ignore
315         self.set_do()
316     def set_do(self):
317         self.__call__ = self.do
318     def set_ignore(self):
319         self.__call__ = self.ignore
320     def set(self, list):
321         if self.type in list:
322             self.set_do()
323         else:
324             self.set_ignore()
325
326 def do_diskcheck_match(node, predicate, errorfmt):
327     result = predicate()
328     try:
329         # If calling the predicate() cached a None value from stat(),
330         # remove it so it doesn't interfere with later attempts to
331         # build this Node as we walk the DAG.  (This isn't a great way
332         # to do this, we're reaching into an interface that doesn't
333         # really belong to us, but it's all about performance, so
334         # for now we'll just document the dependency...)
335         if node._memo['stat'] is None:
336             del node._memo['stat']
337     except (AttributeError, KeyError):
338         pass
339     if result:
340         raise TypeError, errorfmt % node.abspath
341
342 def ignore_diskcheck_match(node, predicate, errorfmt):
343     pass
344
345 def do_diskcheck_rcs(node, name):
346     try:
347         rcs_dir = node.rcs_dir
348     except AttributeError:
349         if node.entry_exists_on_disk('RCS'):
350             rcs_dir = node.Dir('RCS')
351         else:
352             rcs_dir = None
353         node.rcs_dir = rcs_dir
354     if rcs_dir:
355         return rcs_dir.entry_exists_on_disk(name+',v')
356     return None
357
358 def ignore_diskcheck_rcs(node, name):
359     return None
360
361 def do_diskcheck_sccs(node, name):
362     try:
363         sccs_dir = node.sccs_dir
364     except AttributeError:
365         if node.entry_exists_on_disk('SCCS'):
366             sccs_dir = node.Dir('SCCS')
367         else:
368             sccs_dir = None
369         node.sccs_dir = sccs_dir
370     if sccs_dir:
371         return sccs_dir.entry_exists_on_disk('s.'+name)
372     return None
373
374 def ignore_diskcheck_sccs(node, name):
375     return None
376
377 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
378 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
379 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
380
381 diskcheckers = [
382     diskcheck_match,
383     diskcheck_rcs,
384     diskcheck_sccs,
385 ]
386
387 def set_diskcheck(list):
388     for dc in diskcheckers:
389         dc.set(list)
390
391 def diskcheck_types():
392     return map(lambda dc: dc.type, diskcheckers)
393
394
395
396 class EntryProxy(SCons.Util.Proxy):
397     def __get_abspath(self):
398         entry = self.get()
399         return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
400                                              entry.name + "_abspath")
401
402     def __get_filebase(self):
403         name = self.get().name
404         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
405                                              name + "_filebase")
406
407     def __get_suffix(self):
408         name = self.get().name
409         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
410                                              name + "_suffix")
411
412     def __get_file(self):
413         name = self.get().name
414         return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
415
416     def __get_base_path(self):
417         """Return the file's directory and file name, with the
418         suffix stripped."""
419         entry = self.get()
420         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
421                                              entry.name + "_base")
422
423     def __get_posix_path(self):
424         """Return the path with / as the path separator,
425         regardless of platform."""
426         if os.sep == '/':
427             return self
428         else:
429             entry = self.get()
430             r = string.replace(entry.get_path(), os.sep, '/')
431             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
432
433     def __get_windows_path(self):
434         """Return the path with \ as the path separator,
435         regardless of platform."""
436         if os.sep == '\\':
437             return self
438         else:
439             entry = self.get()
440             r = string.replace(entry.get_path(), os.sep, '\\')
441             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
442
443     def __get_srcnode(self):
444         return EntryProxy(self.get().srcnode())
445
446     def __get_srcdir(self):
447         """Returns the directory containing the source node linked to this
448         node via VariantDir(), or the directory of this node if not linked."""
449         return EntryProxy(self.get().srcnode().dir)
450
451     def __get_rsrcnode(self):
452         return EntryProxy(self.get().srcnode().rfile())
453
454     def __get_rsrcdir(self):
455         """Returns the directory containing the source node linked to this
456         node via VariantDir(), or the directory of this node if not linked."""
457         return EntryProxy(self.get().srcnode().rfile().dir)
458
459     def __get_dir(self):
460         return EntryProxy(self.get().dir)
461
462     dictSpecialAttrs = { "base"     : __get_base_path,
463                          "posix"    : __get_posix_path,
464                          "windows"  : __get_windows_path,
465                          "win32"    : __get_windows_path,
466                          "srcpath"  : __get_srcnode,
467                          "srcdir"   : __get_srcdir,
468                          "dir"      : __get_dir,
469                          "abspath"  : __get_abspath,
470                          "filebase" : __get_filebase,
471                          "suffix"   : __get_suffix,
472                          "file"     : __get_file,
473                          "rsrcpath" : __get_rsrcnode,
474                          "rsrcdir"  : __get_rsrcdir,
475                        }
476
477     def __getattr__(self, name):
478         # This is how we implement the "special" attributes
479         # such as base, posix, srcdir, etc.
480         try:
481             attr_function = self.dictSpecialAttrs[name]
482         except KeyError:
483             try:
484                 attr = SCons.Util.Proxy.__getattr__(self, name)
485             except AttributeError:
486                 entry = self.get()
487                 classname = string.split(str(entry.__class__), '.')[-1]
488                 if classname[-2:] == "'>":
489                     # new-style classes report their name as:
490                     #   "<class 'something'>"
491                     # instead of the classic classes:
492                     #   "something"
493                     classname = classname[:-2]
494                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
495             return attr
496         else:
497             return attr_function(self)
498
499 class Base(SCons.Node.Node):
500     """A generic class for file system entries.  This class is for
501     when we don't know yet whether the entry being looked up is a file
502     or a directory.  Instances of this class can morph into either
503     Dir or File objects by a later, more precise lookup.
504
505     Note: this class does not define __cmp__ and __hash__ for
506     efficiency reasons.  SCons does a lot of comparing of
507     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
508     as fast as possible, which means we want to use Python's built-in
509     object identity comparisons.
510     """
511
512     memoizer_counters = []
513
514     def __init__(self, name, directory, fs):
515         """Initialize a generic Node.FS.Base object.
516
517         Call the superclass initialization, take care of setting up
518         our relative and absolute paths, identify our parent
519         directory, and indicate that this node should use
520         signatures."""
521         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
522         SCons.Node.Node.__init__(self)
523
524         self.name = name
525         self.suffix = SCons.Util.splitext(name)[1]
526         self.fs = fs
527
528         assert directory, "A directory must be provided"
529
530         self.abspath = directory.entry_abspath(name)
531         self.labspath = directory.entry_labspath(name)
532         if directory.path == '.':
533             self.path = name
534         else:
535             self.path = directory.entry_path(name)
536         if directory.tpath == '.':
537             self.tpath = name
538         else:
539             self.tpath = directory.entry_tpath(name)
540         self.path_elements = directory.path_elements + [self]
541
542         self.dir = directory
543         self.cwd = None # will hold the SConscript directory for target nodes
544         self.duplicate = directory.duplicate
545
546     def must_be_same(self, klass):
547         """
548         This node, which already existed, is being looked up as the
549         specified klass.  Raise an exception if it isn't.
550         """
551         if self.__class__ is klass or klass is Entry:
552             return
553         raise TypeError, "Tried to lookup %s '%s' as a %s." %\
554               (self.__class__.__name__, self.path, klass.__name__)
555
556     def get_dir(self):
557         return self.dir
558
559     def get_suffix(self):
560         return self.suffix
561
562     def rfile(self):
563         return self
564
565     def __str__(self):
566         """A Node.FS.Base object's string representation is its path
567         name."""
568         global Save_Strings
569         if Save_Strings:
570             return self._save_str()
571         return self._get_str()
572
573     memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
574
575     def _save_str(self):
576         try:
577             return self._memo['_save_str']
578         except KeyError:
579             pass
580         result = self._get_str()
581         self._memo['_save_str'] = result
582         return result
583
584     def _get_str(self):
585         global Save_Strings
586         if self.duplicate or self.is_derived():
587             return self.get_path()
588         srcnode = self.srcnode()
589         if srcnode.stat() is None and not self.stat() is None:
590             result = self.get_path()
591         else:
592             result = srcnode.get_path()
593         if not Save_Strings:
594             # We're not at the point where we're saving the string string
595             # representations of FS Nodes (because we haven't finished
596             # reading the SConscript files and need to have str() return
597             # things relative to them).  That also means we can't yet
598             # cache values returned (or not returned) by stat(), since
599             # Python code in the SConscript files might still create
600             # or otherwise affect the on-disk file.  So get rid of the
601             # values that the underlying stat() method saved.
602             try: del self._memo['stat']
603             except KeyError: pass
604             if not self is srcnode:
605                 try: del srcnode._memo['stat']
606                 except KeyError: pass
607         return result
608
609     rstr = __str__
610
611     memoizer_counters.append(SCons.Memoize.CountValue('stat'))
612
613     def stat(self):
614         try: return self._memo['stat']
615         except KeyError: pass
616         try: result = self.fs.stat(self.abspath)
617         except os.error: result = None
618         self._memo['stat'] = result
619         return result
620
621     def exists(self):
622         return not self.stat() is None
623
624     def rexists(self):
625         return self.rfile().exists()
626
627     def getmtime(self):
628         st = self.stat()
629         if st: return st[stat.ST_MTIME]
630         else: return None
631
632     def getsize(self):
633         st = self.stat()
634         if st: return st[stat.ST_SIZE]
635         else: return None
636
637     def isdir(self):
638         st = self.stat()
639         return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
640
641     def isfile(self):
642         st = self.stat()
643         return not st is None and stat.S_ISREG(st[stat.ST_MODE])
644
645     if hasattr(os, 'symlink'):
646         def islink(self):
647             try: st = self.fs.lstat(self.abspath)
648             except os.error: return 0
649             return stat.S_ISLNK(st[stat.ST_MODE])
650     else:
651         def islink(self):
652             return 0                    # no symlinks
653
654     def is_under(self, dir):
655         if self is dir:
656             return 1
657         else:
658             return self.dir.is_under(dir)
659
660     def set_local(self):
661         self._local = 1
662
663     def srcnode(self):
664         """If this node is in a build path, return the node
665         corresponding to its source file.  Otherwise, return
666         ourself.
667         """
668         srcdir_list = self.dir.srcdir_list()
669         if srcdir_list:
670             srcnode = srcdir_list[0].Entry(self.name)
671             srcnode.must_be_same(self.__class__)
672             return srcnode
673         return self
674
675     def get_path(self, dir=None):
676         """Return path relative to the current working directory of the
677         Node.FS.Base object that owns us."""
678         if not dir:
679             dir = self.fs.getcwd()
680         if self == dir:
681             return '.'
682         path_elems = self.path_elements
683         try: i = path_elems.index(dir)
684         except ValueError: pass
685         else: path_elems = path_elems[i+1:]
686         path_elems = map(lambda n: n.name, path_elems)
687         return string.join(path_elems, os.sep)
688
689     def set_src_builder(self, builder):
690         """Set the source code builder for this node."""
691         self.sbuilder = builder
692         if not self.has_builder():
693             self.builder_set(builder)
694
695     def src_builder(self):
696         """Fetch the source code builder for this node.
697
698         If there isn't one, we cache the source code builder specified
699         for the directory (which in turn will cache the value from its
700         parent directory, and so on up to the file system root).
701         """
702         try:
703             scb = self.sbuilder
704         except AttributeError:
705             scb = self.dir.src_builder()
706             self.sbuilder = scb
707         return scb
708
709     def get_abspath(self):
710         """Get the absolute path of the file."""
711         return self.abspath
712
713     def for_signature(self):
714         # Return just our name.  Even an absolute path would not work,
715         # because that can change thanks to symlinks or remapped network
716         # paths.
717         return self.name
718
719     def get_subst_proxy(self):
720         try:
721             return self._proxy
722         except AttributeError:
723             ret = EntryProxy(self)
724             self._proxy = ret
725             return ret
726
727     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
728         """
729
730         Generates a target entry that corresponds to this entry (usually
731         a source file) with the specified prefix and suffix.
732
733         Note that this method can be overridden dynamically for generated
734         files that need different behavior.  See Tool/swig.py for
735         an example.
736         """
737         return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
738
739     def _Rfindalldirs_key(self, pathlist):
740         return pathlist
741
742     memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
743
744     def Rfindalldirs(self, pathlist):
745         """
746         Return all of the directories for a given path list, including
747         corresponding "backing" directories in any repositories.
748
749         The Node lookups are relative to this Node (typically a
750         directory), so memoizing result saves cycles from looking
751         up the same path for each target in a given directory.
752         """
753         try:
754             memo_dict = self._memo['Rfindalldirs']
755         except KeyError:
756             memo_dict = {}
757             self._memo['Rfindalldirs'] = memo_dict
758         else:
759             try:
760                 return memo_dict[pathlist]
761             except KeyError:
762                 pass
763
764         create_dir_relative_to_self = self.Dir
765         result = []
766         for path in pathlist:
767             if isinstance(path, SCons.Node.Node):
768                 result.append(path)
769             else:
770                 dir = create_dir_relative_to_self(path)
771                 result.extend(dir.get_all_rdirs())
772
773         memo_dict[pathlist] = result
774
775         return result
776
777     def RDirs(self, pathlist):
778         """Search for a list of directories in the Repository list."""
779         cwd = self.cwd or self.fs._cwd
780         return cwd.Rfindalldirs(pathlist)
781
782     memoizer_counters.append(SCons.Memoize.CountValue('rentry'))
783
784     def rentry(self):
785         try:
786             return self._memo['rentry']
787         except KeyError:
788             pass
789         result = self
790         if not self.exists():
791             norm_name = _my_normcase(self.name)
792             for dir in self.dir.get_all_rdirs():
793                 try:
794                     node = dir.entries[norm_name]
795                 except KeyError:
796                     if dir.entry_exists_on_disk(self.name):
797                         result = dir.Entry(self.name)
798                         break
799         self._memo['rentry'] = result
800         return result
801
802     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
803         return []
804
805 class Entry(Base):
806     """This is the class for generic Node.FS entries--that is, things
807     that could be a File or a Dir, but we're just not sure yet.
808     Consequently, the methods in this class really exist just to
809     transform their associated object into the right class when the
810     time comes, and then call the same-named method in the transformed
811     class."""
812
813     def diskcheck_match(self):
814         pass
815
816     def disambiguate(self, must_exist=None):
817         """
818         """
819         if self.isdir():
820             self.__class__ = Dir
821             self._morph()
822         elif self.isfile():
823             self.__class__ = File
824             self._morph()
825             self.clear()
826         else:
827             # There was nothing on-disk at this location, so look in
828             # the src directory.
829             #
830             # We can't just use self.srcnode() straight away because
831             # that would create an actual Node for this file in the src
832             # directory, and there might not be one.  Instead, use the
833             # dir_on_disk() method to see if there's something on-disk
834             # with that name, in which case we can go ahead and call
835             # self.srcnode() to create the right type of entry.
836             srcdir = self.dir.srcnode()
837             if srcdir != self.dir and \
838                srcdir.entry_exists_on_disk(self.name) and \
839                self.srcnode().isdir():
840                 self.__class__ = Dir
841                 self._morph()
842             elif must_exist:
843                 msg = "No such file or directory: '%s'" % self.abspath
844                 raise SCons.Errors.UserError, msg
845             else:
846                 self.__class__ = File
847                 self._morph()
848                 self.clear()
849         return self
850
851     def rfile(self):
852         """We're a generic Entry, but the caller is actually looking for
853         a File at this point, so morph into one."""
854         self.__class__ = File
855         self._morph()
856         self.clear()
857         return File.rfile(self)
858
859     def scanner_key(self):
860         return self.get_suffix()
861
862     def get_contents(self):
863         """Fetch the contents of the entry.
864
865         Since this should return the real contents from the file
866         system, we check to see into what sort of subclass we should
867         morph this Entry."""
868         try:
869             self = self.disambiguate(must_exist=1)
870         except SCons.Errors.UserError:
871             # There was nothing on disk with which to disambiguate
872             # this entry.  Leave it as an Entry, but return a null
873             # string so calls to get_contents() in emitters and the
874             # like (e.g. in qt.py) don't have to disambiguate by hand
875             # or catch the exception.
876             return ''
877         else:
878             return self.get_contents()
879
880     def must_be_same(self, klass):
881         """Called to make sure a Node is a Dir.  Since we're an
882         Entry, we can morph into one."""
883         if not self.__class__ is klass:
884             self.__class__ = klass
885             self._morph()
886             self.clear
887
888     # The following methods can get called before the Taskmaster has
889     # had a chance to call disambiguate() directly to see if this Entry
890     # should really be a Dir or a File.  We therefore use these to call
891     # disambiguate() transparently (from our caller's point of view).
892     #
893     # Right now, this minimal set of methods has been derived by just
894     # looking at some of the methods that will obviously be called early
895     # in any of the various Taskmasters' calling sequences, and then
896     # empirically figuring out which additional methods are necessary
897     # to make various tests pass.
898
899     def exists(self):
900         """Return if the Entry exists.  Check the file system to see
901         what we should turn into first.  Assume a file if there's no
902         directory."""
903         return self.disambiguate().exists()
904
905     def rel_path(self, other):
906         d = self.disambiguate()
907         if d.__class__ == Entry:
908             raise "rel_path() could not disambiguate File/Dir"
909         return d.rel_path(other)
910
911     def new_ninfo(self):
912         return self.disambiguate().new_ninfo()
913
914     def changed_since_last_build(self, target, prev_ni):
915         return self.disambiguate().changed_since_last_build(target, prev_ni)
916
917     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
918         return self.disambiguate()._glob1(pattern, ondisk, source, strings)
919
920 # This is for later so we can differentiate between Entry the class and Entry
921 # the method of the FS class.
922 _classEntry = Entry
923
924
925 class LocalFS:
926
927     if SCons.Memoize.use_memoizer:
928         __metaclass__ = SCons.Memoize.Memoized_Metaclass
929
930     # This class implements an abstraction layer for operations involving
931     # a local file system.  Essentially, this wraps any function in
932     # the os, os.path or shutil modules that we use to actually go do
933     # anything with or to the local file system.
934     #
935     # Note that there's a very good chance we'll refactor this part of
936     # the architecture in some way as we really implement the interface(s)
937     # for remote file system Nodes.  For example, the right architecture
938     # might be to have this be a subclass instead of a base class.
939     # Nevertheless, we're using this as a first step in that direction.
940     #
941     # We're not using chdir() yet because the calling subclass method
942     # needs to use os.chdir() directly to avoid recursion.  Will we
943     # really need this one?
944     #def chdir(self, path):
945     #    return os.chdir(path)
946     def chmod(self, path, mode):
947         return os.chmod(path, mode)
948     def copy(self, src, dst):
949         return shutil.copy(src, dst)
950     def copy2(self, src, dst):
951         return shutil.copy2(src, dst)
952     def exists(self, path):
953         return os.path.exists(path)
954     def getmtime(self, path):
955         return os.path.getmtime(path)
956     def getsize(self, path):
957         return os.path.getsize(path)
958     def isdir(self, path):
959         return os.path.isdir(path)
960     def isfile(self, path):
961         return os.path.isfile(path)
962     def link(self, src, dst):
963         return os.link(src, dst)
964     def lstat(self, path):
965         return os.lstat(path)
966     def listdir(self, path):
967         return os.listdir(path)
968     def makedirs(self, path):
969         return os.makedirs(path)
970     def mkdir(self, path):
971         return os.mkdir(path)
972     def rename(self, old, new):
973         return os.rename(old, new)
974     def stat(self, path):
975         return os.stat(path)
976     def symlink(self, src, dst):
977         return os.symlink(src, dst)
978     def open(self, path):
979         return open(path)
980     def unlink(self, path):
981         return os.unlink(path)
982
983     if hasattr(os, 'symlink'):
984         def islink(self, path):
985             return os.path.islink(path)
986     else:
987         def islink(self, path):
988             return 0                    # no symlinks
989
990     if hasattr(os, 'readlink'):
991         def readlink(self, file):
992             return os.readlink(file)
993     else:
994         def readlink(self, file):
995             return ''
996
997
998 #class RemoteFS:
999 #    # Skeleton for the obvious methods we might need from the
1000 #    # abstraction layer for a remote filesystem.
1001 #    def upload(self, local_src, remote_dst):
1002 #        pass
1003 #    def download(self, remote_src, local_dst):
1004 #        pass
1005
1006
1007 class FS(LocalFS):
1008
1009     memoizer_counters = []
1010
1011     def __init__(self, path = None):
1012         """Initialize the Node.FS subsystem.
1013
1014         The supplied path is the top of the source tree, where we
1015         expect to find the top-level build file.  If no path is
1016         supplied, the current directory is the default.
1017
1018         The path argument must be a valid absolute path.
1019         """
1020         if __debug__: logInstanceCreation(self, 'Node.FS')
1021
1022         self._memo = {}
1023
1024         self.Root = {}
1025         self.SConstruct_dir = None
1026         self.max_drift = default_max_drift
1027
1028         self.Top = None
1029         if path is None:
1030             self.pathTop = os.getcwd()
1031         else:
1032             self.pathTop = path
1033         self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
1034
1035         self.Top = self.Dir(self.pathTop)
1036         self.Top.path = '.'
1037         self.Top.tpath = '.'
1038         self._cwd = self.Top
1039
1040         DirNodeInfo.fs = self
1041         FileNodeInfo.fs = self
1042     
1043     def set_SConstruct_dir(self, dir):
1044         self.SConstruct_dir = dir
1045
1046     def get_max_drift(self):
1047         return self.max_drift
1048
1049     def set_max_drift(self, max_drift):
1050         self.max_drift = max_drift
1051
1052     def getcwd(self):
1053         return self._cwd
1054
1055     def chdir(self, dir, change_os_dir=0):
1056         """Change the current working directory for lookups.
1057         If change_os_dir is true, we will also change the "real" cwd
1058         to match.
1059         """
1060         curr=self._cwd
1061         try:
1062             if not dir is None:
1063                 self._cwd = dir
1064                 if change_os_dir:
1065                     os.chdir(dir.abspath)
1066         except OSError:
1067             self._cwd = curr
1068             raise
1069
1070     def get_root(self, drive):
1071         """
1072         Returns the root directory for the specified drive, creating
1073         it if necessary.
1074         """
1075         drive = _my_normcase(drive)
1076         try:
1077             return self.Root[drive]
1078         except KeyError:
1079             root = RootDir(drive, self)
1080             self.Root[drive] = root
1081             if not drive:
1082                 self.Root[self.defaultDrive] = root
1083             elif drive == self.defaultDrive:
1084                 self.Root[''] = root
1085             return root
1086
1087     def _lookup(self, p, directory, fsclass, create=1):
1088         """
1089         The generic entry point for Node lookup with user-supplied data.
1090
1091         This translates arbitrary input into a canonical Node.FS object
1092         of the specified fsclass.  The general approach for strings is
1093         to turn it into a fully normalized absolute path and then call
1094         the root directory's lookup_abs() method for the heavy lifting.
1095
1096         If the path name begins with '#', it is unconditionally
1097         interpreted relative to the top-level directory of this FS.  '#'
1098         is treated as a synonym for the top-level SConstruct directory,
1099         much like '~' is treated as a synonym for the user's home
1100         directory in a UNIX shell.  So both '#foo' and '#/foo' refer
1101         to the 'foo' subdirectory underneath the top-level SConstruct
1102         directory.
1103
1104         If the path name is relative, then the path is looked up relative
1105         to the specified directory, or the current directory (self._cwd,
1106         typically the SConscript directory) if the specified directory
1107         is None.
1108         """
1109         if isinstance(p, Base):
1110             # It's already a Node.FS object.  Make sure it's the right
1111             # class and return.
1112             p.must_be_same(fsclass)
1113             return p
1114         # str(p) in case it's something like a proxy object
1115         p = str(p)
1116
1117         initial_hash = (p[0:1] == '#')
1118         if initial_hash:
1119             # There was an initial '#', so we strip it and override
1120             # whatever directory they may have specified with the
1121             # top-level SConstruct directory.
1122             p = p[1:]
1123             directory = self.Top
1124
1125         if directory and not isinstance(directory, Dir):
1126             directory = self.Dir(directory)
1127
1128         if do_splitdrive:
1129             drive, p = os.path.splitdrive(p)
1130         else:
1131             drive = ''
1132         if drive and not p:
1133             # This causes a naked drive letter to be treated as a synonym
1134             # for the root directory on that drive.
1135             p = os.sep
1136         absolute = os.path.isabs(p)
1137
1138         needs_normpath = needs_normpath_check.match(p)
1139
1140         if initial_hash or not absolute:
1141             # This is a relative lookup, either to the top-level
1142             # SConstruct directory (because of the initial '#') or to
1143             # the current directory (the path name is not absolute).
1144             # Add the string to the appropriate directory lookup path,
1145             # after which the whole thing gets normalized.
1146             if not directory:
1147                 directory = self._cwd
1148             if p:
1149                 p = directory.labspath + '/' + p
1150             else:
1151                 p = directory.labspath
1152
1153         if needs_normpath:
1154             p = os.path.normpath(p)
1155
1156         if drive or absolute:
1157             root = self.get_root(drive)
1158         else:
1159             if not directory:
1160                 directory = self._cwd
1161             root = directory.root
1162
1163         if os.sep != '/':
1164             p = string.replace(p, os.sep, '/')
1165         return root._lookup_abs(p, fsclass, create)
1166
1167     def Entry(self, name, directory = None, create = 1):
1168         """Lookup or create a generic Entry node with the specified name.
1169         If the name is a relative path (begins with ./, ../, or a file
1170         name), then it is looked up relative to the supplied directory
1171         node, or to the top level directory of the FS (supplied at
1172         construction time) if no directory is supplied.
1173         """
1174         return self._lookup(name, directory, Entry, create)
1175
1176     def File(self, name, directory = None, create = 1):
1177         """Lookup or create a File node with the specified name.  If
1178         the name is a relative path (begins with ./, ../, or a file name),
1179         then it is looked up relative to the supplied directory node,
1180         or to the top level directory of the FS (supplied at construction
1181         time) if no directory is supplied.
1182
1183         This method will raise TypeError if a directory is found at the
1184         specified path.
1185         """
1186         return self._lookup(name, directory, File, create)
1187
1188     def Dir(self, name, directory = None, create = True):
1189         """Lookup or create a Dir node with the specified name.  If
1190         the name is a relative path (begins with ./, ../, or a file name),
1191         then it is looked up relative to the supplied directory node,
1192         or to the top level directory of the FS (supplied at construction
1193         time) if no directory is supplied.
1194
1195         This method will raise TypeError if a normal file is found at the
1196         specified path.
1197         """
1198         return self._lookup(name, directory, Dir, create)
1199
1200     def VariantDir(self, variant_dir, src_dir, duplicate=1):
1201         """Link the supplied variant directory to the source directory
1202         for purposes of building files."""
1203
1204         if not isinstance(src_dir, SCons.Node.Node):
1205             src_dir = self.Dir(src_dir)
1206         if not isinstance(variant_dir, SCons.Node.Node):
1207             variant_dir = self.Dir(variant_dir)
1208         if src_dir.is_under(variant_dir):
1209             raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
1210         if variant_dir.srcdir:
1211             if variant_dir.srcdir == src_dir:
1212                 return # We already did this.
1213             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
1214         variant_dir.link(src_dir, duplicate)
1215
1216     def Repository(self, *dirs):
1217         """Specify Repository directories to search."""
1218         for d in dirs:
1219             if not isinstance(d, SCons.Node.Node):
1220                 d = self.Dir(d)
1221             self.Top.addRepository(d)
1222
1223     def variant_dir_target_climb(self, orig, dir, tail):
1224         """Create targets in corresponding variant directories
1225
1226         Climb the directory tree, and look up path names
1227         relative to any linked variant directories we find.
1228
1229         Even though this loops and walks up the tree, we don't memoize
1230         the return value because this is really only used to process
1231         the command-line targets.
1232         """
1233         targets = []
1234         message = None
1235         fmt = "building associated VariantDir targets: %s"
1236         start_dir = dir
1237         while dir:
1238             for bd in dir.variant_dirs:
1239                 if start_dir.is_under(bd):
1240                     # If already in the build-dir location, don't reflect
1241                     return [orig], fmt % str(orig)
1242                 p = apply(os.path.join, [bd.path] + tail)
1243                 targets.append(self.Entry(p))
1244             tail = [dir.name] + tail
1245             dir = dir.up()
1246         if targets:
1247             message = fmt % string.join(map(str, targets))
1248         return targets, message
1249
1250     def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
1251         """
1252         Globs
1253
1254         This is mainly a shim layer 
1255         """
1256         if cwd is None:
1257             cwd = self.getcwd()
1258         return cwd.glob(pathname, ondisk, source, strings)
1259
1260 class DirNodeInfo(SCons.Node.NodeInfoBase):
1261     # This should get reset by the FS initialization.
1262     current_version_id = 1
1263
1264     fs = None
1265
1266     def str_to_node(self, s):
1267         top = self.fs.Top
1268         root = top.root
1269         if do_splitdrive:
1270             drive, s = os.path.splitdrive(s)
1271             if drive:
1272                 root = self.fs.get_root(drive)
1273         if not os.path.isabs(s):
1274             s = top.labspath + '/' + s
1275         return root._lookup_abs(s, Entry)
1276
1277 class DirBuildInfo(SCons.Node.BuildInfoBase):
1278     current_version_id = 1
1279
1280 glob_magic_check = re.compile('[*?[]')
1281
1282 def has_glob_magic(s):
1283     return glob_magic_check.search(s) is not None
1284
1285 class Dir(Base):
1286     """A class for directories in a file system.
1287     """
1288
1289     memoizer_counters = []
1290
1291     NodeInfo = DirNodeInfo
1292     BuildInfo = DirBuildInfo
1293
1294     def __init__(self, name, directory, fs):
1295         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1296         Base.__init__(self, name, directory, fs)
1297         self._morph()
1298
1299     def _morph(self):
1300         """Turn a file system Node (either a freshly initialized directory
1301         object or a separate Entry object) into a proper directory object.
1302
1303         Set up this directory's entries and hook it into the file
1304         system tree.  Specify that directories (this Node) don't use
1305         signatures for calculating whether they're current.
1306         """
1307
1308         self.repositories = []
1309         self.srcdir = None
1310
1311         self.entries = {}
1312         self.entries['.'] = self
1313         self.entries['..'] = self.dir
1314         self.cwd = self
1315         self.searched = 0
1316         self._sconsign = None
1317         self.variant_dirs = []
1318         self.root = self.dir.root
1319
1320         # Don't just reset the executor, replace its action list,
1321         # because it might have some pre-or post-actions that need to
1322         # be preserved.
1323         self.builder = get_MkdirBuilder()
1324         self.get_executor().set_action_list(self.builder.action)
1325
1326     def diskcheck_match(self):
1327         diskcheck_match(self, self.isfile,
1328                         "File %s found where directory expected.")
1329
1330     def __clearRepositoryCache(self, duplicate=None):
1331         """Called when we change the repository(ies) for a directory.
1332         This clears any cached information that is invalidated by changing
1333         the repository."""
1334
1335         for node in self.entries.values():
1336             if node != self.dir:
1337                 if node != self and isinstance(node, Dir):
1338                     node.__clearRepositoryCache(duplicate)
1339                 else:
1340                     node.clear()
1341                     try:
1342                         del node._srcreps
1343                     except AttributeError:
1344                         pass
1345                     if duplicate != None:
1346                         node.duplicate=duplicate
1347
1348     def __resetDuplicate(self, node):
1349         if node != self:
1350             node.duplicate = node.get_dir().duplicate
1351
1352     def Entry(self, name):
1353         """
1354         Looks up or creates an entry node named 'name' relative to
1355         this directory.
1356         """
1357         return self.fs.Entry(name, self)
1358
1359     def Dir(self, name, create=True):
1360         """
1361         Looks up or creates a directory node named 'name' relative to
1362         this directory.
1363         """
1364         dir = self.fs.Dir(name, self, create)
1365         return dir
1366
1367     def File(self, name):
1368         """
1369         Looks up or creates a file node named 'name' relative to
1370         this directory.
1371         """
1372         return self.fs.File(name, self)
1373
1374     def _lookup_rel(self, name, klass, create=1):
1375         """
1376         Looks up a *normalized* relative path name, relative to this
1377         directory.
1378
1379         This method is intended for use by internal lookups with
1380         already-normalized path data.  For general-purpose lookups,
1381         use the Entry(), Dir() and File() methods above.
1382
1383         This method does *no* input checking and will die or give
1384         incorrect results if it's passed a non-normalized path name (e.g.,
1385         a path containing '..'), an absolute path name, a top-relative
1386         ('#foo') path name, or any kind of object.
1387         """
1388         name = self.entry_labspath(name)
1389         return self.root._lookup_abs(name, klass, create)
1390
1391     def link(self, srcdir, duplicate):
1392         """Set this directory as the variant directory for the
1393         supplied source directory."""
1394         self.srcdir = srcdir
1395         self.duplicate = duplicate
1396         self.__clearRepositoryCache(duplicate)
1397         srcdir.variant_dirs.append(self)
1398
1399     def getRepositories(self):
1400         """Returns a list of repositories for this directory.
1401         """
1402         if self.srcdir and not self.duplicate:
1403             return self.srcdir.get_all_rdirs() + self.repositories
1404         return self.repositories
1405
1406     memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1407
1408     def get_all_rdirs(self):
1409         try:
1410             return self._memo['get_all_rdirs']
1411         except KeyError:
1412             pass
1413
1414         result = [self]
1415         fname = '.'
1416         dir = self
1417         while dir:
1418             for rep in dir.getRepositories():
1419                 result.append(rep.Dir(fname))
1420             if fname == '.':
1421                 fname = dir.name
1422             else:
1423                 fname = dir.name + os.sep + fname
1424             dir = dir.up()
1425
1426         self._memo['get_all_rdirs'] = result
1427
1428         return result
1429
1430     def addRepository(self, dir):
1431         if dir != self and not dir in self.repositories:
1432             self.repositories.append(dir)
1433             dir.tpath = '.'
1434             self.__clearRepositoryCache()
1435
1436     def up(self):
1437         return self.entries['..']
1438
1439     def _rel_path_key(self, other):
1440         return str(other)
1441
1442     memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1443
1444     def rel_path(self, other):
1445         """Return a path to "other" relative to this directory.
1446         """
1447
1448         # This complicated and expensive method, which constructs relative
1449         # paths between arbitrary Node.FS objects, is no longer used
1450         # by SCons itself.  It was introduced to store dependency paths
1451         # in .sconsign files relative to the target, but that ended up
1452         # being significantly inefficient.
1453         #
1454         # We're continuing to support the method because some SConstruct
1455         # files out there started using it when it was available, and
1456         # we're all about backwards compatibility..
1457
1458         try:
1459             memo_dict = self._memo['rel_path']
1460         except KeyError:
1461             memo_dict = {}
1462             self._memo['rel_path'] = memo_dict
1463         else:
1464             try:
1465                 return memo_dict[other]
1466             except KeyError:
1467                 pass
1468
1469         if self is other:
1470
1471             result = '.'
1472
1473         elif not other in self.path_elements:
1474
1475             try:
1476                 other_dir = other.get_dir()
1477             except AttributeError:
1478                 result = str(other)
1479             else:
1480                 if other_dir is None:
1481                     result = other.name
1482                 else:
1483                     dir_rel_path = self.rel_path(other_dir)
1484                     if dir_rel_path == '.':
1485                         result = other.name
1486                     else:
1487                         result = dir_rel_path + os.sep + other.name
1488
1489         else:
1490
1491             i = self.path_elements.index(other) + 1
1492
1493             path_elems = ['..'] * (len(self.path_elements) - i) \
1494                          + map(lambda n: n.name, other.path_elements[i:])
1495              
1496             result = string.join(path_elems, os.sep)
1497
1498         memo_dict[other] = result
1499
1500         return result
1501
1502     def get_env_scanner(self, env, kw={}):
1503         import SCons.Defaults
1504         return SCons.Defaults.DirEntryScanner
1505
1506     def get_target_scanner(self):
1507         import SCons.Defaults
1508         return SCons.Defaults.DirEntryScanner
1509
1510     def get_found_includes(self, env, scanner, path):
1511         """Return this directory's implicit dependencies.
1512
1513         We don't bother caching the results because the scan typically
1514         shouldn't be requested more than once (as opposed to scanning
1515         .h file contents, which can be requested as many times as the
1516         files is #included by other files).
1517         """
1518         if not scanner:
1519             return []
1520         # Clear cached info for this Dir.  If we already visited this
1521         # directory on our walk down the tree (because we didn't know at
1522         # that point it was being used as the source for another Node)
1523         # then we may have calculated build signature before realizing
1524         # we had to scan the disk.  Now that we have to, though, we need
1525         # to invalidate the old calculated signature so that any node
1526         # dependent on our directory structure gets one that includes
1527         # info about everything on disk.
1528         self.clear()
1529         return scanner(self, env, path)
1530
1531     #
1532     # Taskmaster interface subsystem
1533     #
1534
1535     def prepare(self):
1536         pass
1537
1538     def build(self, **kw):
1539         """A null "builder" for directories."""
1540         global MkdirBuilder
1541         if not self.builder is MkdirBuilder:
1542             apply(SCons.Node.Node.build, [self,], kw)
1543
1544     #
1545     #
1546     #
1547
1548     def _create(self):
1549         """Create this directory, silently and without worrying about
1550         whether the builder is the default or not."""
1551         listDirs = []
1552         parent = self
1553         while parent:
1554             if parent.exists():
1555                 break
1556             listDirs.append(parent)
1557             p = parent.up()
1558             if p is None:
1559                 raise SCons.Errors.StopError, parent.path
1560             parent = p
1561         listDirs.reverse()
1562         for dirnode in listDirs:
1563             try:
1564                 # Don't call dirnode.build(), call the base Node method
1565                 # directly because we definitely *must* create this
1566                 # directory.  The dirnode.build() method will suppress
1567                 # the build if it's the default builder.
1568                 SCons.Node.Node.build(dirnode)
1569                 dirnode.get_executor().nullify()
1570                 # The build() action may or may not have actually
1571                 # created the directory, depending on whether the -n
1572                 # option was used or not.  Delete the _exists and
1573                 # _rexists attributes so they can be reevaluated.
1574                 dirnode.clear()
1575             except OSError:
1576                 pass
1577
1578     def multiple_side_effect_has_builder(self):
1579         global MkdirBuilder
1580         return not self.builder is MkdirBuilder and self.has_builder()
1581
1582     def alter_targets(self):
1583         """Return any corresponding targets in a variant directory.
1584         """
1585         return self.fs.variant_dir_target_climb(self, self, [])
1586
1587     def scanner_key(self):
1588         """A directory does not get scanned."""
1589         return None
1590
1591     def get_contents(self):
1592         """Return aggregate contents of all our children."""
1593         contents = map(lambda n: n.get_contents(), self.children())
1594         return  string.join(contents, '')
1595
1596     def do_duplicate(self, src):
1597         pass
1598
1599     changed_since_last_build = SCons.Node.Node.state_has_changed
1600
1601     def is_up_to_date(self):
1602         """If any child is not up-to-date, then this directory isn't,
1603         either."""
1604         if not self.builder is MkdirBuilder and not self.exists():
1605             return 0
1606         up_to_date = SCons.Node.up_to_date
1607         for kid in self.children():
1608             if kid.get_state() > up_to_date:
1609                 return 0
1610         return 1
1611
1612     def rdir(self):
1613         if not self.exists():
1614             norm_name = _my_normcase(self.name)
1615             for dir in self.dir.get_all_rdirs():
1616                 try: node = dir.entries[norm_name]
1617                 except KeyError: node = dir.dir_on_disk(self.name)
1618                 if node and node.exists() and \
1619                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1620                         return node
1621         return self
1622
1623     def sconsign(self):
1624         """Return the .sconsign file info for this directory,
1625         creating it first if necessary."""
1626         if not self._sconsign:
1627             import SCons.SConsign
1628             self._sconsign = SCons.SConsign.ForDirectory(self)
1629         return self._sconsign
1630
1631     def srcnode(self):
1632         """Dir has a special need for srcnode()...if we
1633         have a srcdir attribute set, then that *is* our srcnode."""
1634         if self.srcdir:
1635             return self.srcdir
1636         return Base.srcnode(self)
1637
1638     def get_timestamp(self):
1639         """Return the latest timestamp from among our children"""
1640         stamp = 0
1641         for kid in self.children():
1642             if kid.get_timestamp() > stamp:
1643                 stamp = kid.get_timestamp()
1644         return stamp
1645
1646     def entry_abspath(self, name):
1647         return self.abspath + os.sep + name
1648
1649     def entry_labspath(self, name):
1650         return self.labspath + '/' + name
1651
1652     def entry_path(self, name):
1653         return self.path + os.sep + name
1654
1655     def entry_tpath(self, name):
1656         return self.tpath + os.sep + name
1657
1658     def entry_exists_on_disk(self, name):
1659         try:
1660             d = self.on_disk_entries
1661         except AttributeError:
1662             d = {}
1663             try:
1664                 entries = os.listdir(self.abspath)
1665             except OSError:
1666                 pass
1667             else:
1668                 for entry in map(_my_normcase, entries):
1669                     d[entry] = 1
1670             self.on_disk_entries = d
1671         return d.has_key(_my_normcase(name))
1672
1673     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1674
1675     def srcdir_list(self):
1676         try:
1677             return self._memo['srcdir_list']
1678         except KeyError:
1679             pass
1680
1681         result = []
1682
1683         dirname = '.'
1684         dir = self
1685         while dir:
1686             if dir.srcdir:
1687                 result.append(dir.srcdir.Dir(dirname))
1688             dirname = dir.name + os.sep + dirname
1689             dir = dir.up()
1690
1691         self._memo['srcdir_list'] = result
1692
1693         return result
1694
1695     def srcdir_duplicate(self, name):
1696         for dir in self.srcdir_list():
1697             if self.is_under(dir):
1698                 # We shouldn't source from something in the build path;
1699                 # variant_dir is probably under src_dir, in which case
1700                 # we are reflecting.
1701                 break
1702             if dir.entry_exists_on_disk(name):
1703                 srcnode = dir.Entry(name).disambiguate()
1704                 if self.duplicate:
1705                     node = self.Entry(name).disambiguate()
1706                     node.do_duplicate(srcnode)
1707                     return node
1708                 else:
1709                     return srcnode
1710         return None
1711
1712     def _srcdir_find_file_key(self, filename):
1713         return filename
1714
1715     memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1716
1717     def srcdir_find_file(self, filename):
1718         try:
1719             memo_dict = self._memo['srcdir_find_file']
1720         except KeyError:
1721             memo_dict = {}
1722             self._memo['srcdir_find_file'] = memo_dict
1723         else:
1724             try:
1725                 return memo_dict[filename]
1726             except KeyError:
1727                 pass
1728
1729         def func(node):
1730             if (isinstance(node, File) or isinstance(node, Entry)) and \
1731                (node.is_derived() or node.exists()):
1732                     return node
1733             return None
1734
1735         norm_name = _my_normcase(filename)
1736
1737         for rdir in self.get_all_rdirs():
1738             try: node = rdir.entries[norm_name]
1739             except KeyError: node = rdir.file_on_disk(filename)
1740             else: node = func(node)
1741             if node:
1742                 result = (node, self)
1743                 memo_dict[filename] = result
1744                 return result
1745
1746         for srcdir in self.srcdir_list():
1747             for rdir in srcdir.get_all_rdirs():
1748                 try: node = rdir.entries[norm_name]
1749                 except KeyError: node = rdir.file_on_disk(filename)
1750                 else: node = func(node)
1751                 if node:
1752                     result = (File(filename, self, self.fs), srcdir)
1753                     memo_dict[filename] = result
1754                     return result
1755
1756         result = (None, None)
1757         memo_dict[filename] = result
1758         return result
1759
1760     def dir_on_disk(self, name):
1761         if self.entry_exists_on_disk(name):
1762             try: return self.Dir(name)
1763             except TypeError: pass
1764         return None
1765
1766     def file_on_disk(self, name):
1767         if self.entry_exists_on_disk(name) or \
1768            diskcheck_rcs(self, name) or \
1769            diskcheck_sccs(self, name):
1770             try: return self.File(name)
1771             except TypeError: pass
1772         node = self.srcdir_duplicate(name)
1773         if isinstance(node, Dir):
1774             node = None
1775         return node
1776
1777     def walk(self, func, arg):
1778         """
1779         Walk this directory tree by calling the specified function
1780         for each directory in the tree.
1781
1782         This behaves like the os.path.walk() function, but for in-memory
1783         Node.FS.Dir objects.  The function takes the same arguments as
1784         the functions passed to os.path.walk():
1785
1786                 func(arg, dirname, fnames)
1787
1788         Except that "dirname" will actually be the directory *Node*,
1789         not the string.  The '.' and '..' entries are excluded from
1790         fnames.  The fnames list may be modified in-place to filter the
1791         subdirectories visited or otherwise impose a specific order.
1792         The "arg" argument is always passed to func() and may be used
1793         in any way (or ignored, passing None is common).
1794         """
1795         entries = self.entries
1796         names = entries.keys()
1797         names.remove('.')
1798         names.remove('..')
1799         func(arg, self, names)
1800         select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
1801         for dirname in filter(select_dirs, names):
1802             entries[dirname].walk(func, arg)
1803
1804     def glob(self, pathname, ondisk=True, source=False, strings=False):
1805         """
1806         Returns a list of Nodes (or strings) matching a specified
1807         pathname pattern.
1808
1809         Pathname patterns follow UNIX shell semantics:  * matches
1810         any-length strings of any characters, ? matches any character,
1811         and [] can enclose lists or ranges of characters.  Matches do
1812         not span directory separators.
1813
1814         The matches take into account Repositories, returning local
1815         Nodes if a corresponding entry exists in a Repository (either
1816         an in-memory Node or something on disk).
1817
1818         By defafult, the glob() function matches entries that exist
1819         on-disk, in addition to in-memory Nodes.  Setting the "ondisk"
1820         argument to False (or some other non-true value) causes the glob()
1821         function to only match in-memory Nodes.  The default behavior is
1822         to return both the on-disk and in-memory Nodes.
1823
1824         The "source" argument, when true, specifies that corresponding
1825         source Nodes must be returned if you're globbing in a build
1826         directory (initialized with VariantDir()).  The default behavior
1827         is to return Nodes local to the VariantDir().
1828
1829         The "strings" argument, when true, returns the matches as strings,
1830         not Nodes.  The strings are path names relative to this directory.
1831
1832         The underlying algorithm is adapted from the glob.glob() function
1833         in the Python library (but heavily modified), and uses fnmatch()
1834         under the covers.
1835         """
1836         dirname, basename = os.path.split(pathname)
1837         if not dirname:
1838             return self._glob1(basename, ondisk, source, strings)
1839         if has_glob_magic(dirname):
1840             list = self.glob(dirname, ondisk, source, strings=False)
1841         else:
1842             list = [self.Dir(dirname, create=True)]
1843         result = []
1844         for dir in list:
1845             r = dir._glob1(basename, ondisk, source, strings)
1846             if strings:
1847                 r = map(lambda x, d=str(dir): os.path.join(d, x), r)
1848             result.extend(r)
1849         return result
1850
1851     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1852         """
1853         Globs for and returns a list of entry names matching a single
1854         pattern in this directory.
1855
1856         This searches any repositories and source directories for
1857         corresponding entries and returns a Node (or string) relative
1858         to the current directory if an entry is found anywhere.
1859
1860         TODO: handle pattern with no wildcard
1861         """
1862         search_dir_list = self.get_all_rdirs()
1863         for srcdir in self.srcdir_list():
1864             search_dir_list.extend(srcdir.get_all_rdirs())
1865
1866         names = []
1867         for dir in search_dir_list:
1868             # We use the .name attribute from the Node because the keys of
1869             # the dir.entries dictionary are normalized (that is, all upper
1870             # case) on case-insensitive systems like Windows.
1871             #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ]
1872             entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
1873             node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
1874             names.extend(node_names)
1875             if ondisk:
1876                 try:
1877                     disk_names = os.listdir(dir.abspath)
1878                 except os.error:
1879                     pass
1880                 else:
1881                     names.extend(disk_names)
1882                     if not strings:
1883                         # We're going to return corresponding Nodes in
1884                         # the local directory, so we need to make sure
1885                         # those Nodes exist.  We only want to create
1886                         # Nodes for the entries that will match the
1887                         # specified pattern, though, which means we
1888                         # need to filter the list here, even though
1889                         # the overall list will also be filtered later,
1890                         # after we exit this loop.
1891                         if pattern[0] != '.':
1892                             #disk_names = [ d for d in disk_names if d[0] != '.' ]
1893                             disk_names = filter(lambda x: x[0] != '.', disk_names)
1894                         disk_names = fnmatch.filter(disk_names, pattern)
1895                         rep_nodes = map(dir.Entry, disk_names)
1896                         #rep_nodes = [ n.disambiguate() for n in rep_nodes ]
1897                         rep_nodes = map(lambda n: n.disambiguate(), rep_nodes)
1898                         for node, name in zip(rep_nodes, disk_names):
1899                             n = self.Entry(name)
1900                             if n.__class__ != node.__class__:
1901                                 n.__class__ = node.__class__
1902                                 n._morph()
1903
1904         names = set(names)
1905         if pattern[0] != '.':
1906             #names = [ n for n in names if n[0] != '.' ]
1907             names = filter(lambda x: x[0] != '.', names)
1908         names = fnmatch.filter(names, pattern)
1909
1910         if strings:
1911             return names
1912
1913         #return [ self.entries[_my_normcase(n)] for n in names ]
1914         return map(lambda n, e=self.entries:  e[_my_normcase(n)], names)
1915
1916 class RootDir(Dir):
1917     """A class for the root directory of a file system.
1918
1919     This is the same as a Dir class, except that the path separator
1920     ('/' or '\\') is actually part of the name, so we don't need to
1921     add a separator when creating the path names of entries within
1922     this directory.
1923     """
1924     def __init__(self, name, fs):
1925         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1926         # We're going to be our own parent directory (".." entry and .dir
1927         # attribute) so we have to set up some values so Base.__init__()
1928         # won't gag won't it calls some of our methods.
1929         self.abspath = ''
1930         self.labspath = ''
1931         self.path = ''
1932         self.tpath = ''
1933         self.path_elements = []
1934         self.duplicate = 0
1935         self.root = self
1936         Base.__init__(self, name, self, fs)
1937
1938         # Now set our paths to what we really want them to be: the
1939         # initial drive letter (the name) plus the directory separator,
1940         # except for the "lookup abspath," which does not have the
1941         # drive letter.
1942         self.abspath = name + os.sep
1943         self.labspath = ''
1944         self.path = name + os.sep
1945         self.tpath = name + os.sep
1946         self._morph()
1947
1948         self._lookupDict = {}
1949
1950         # The // and os.sep + os.sep entries are necessary because
1951         # os.path.normpath() seems to preserve double slashes at the
1952         # beginning of a path (presumably for UNC path names), but
1953         # collapses triple slashes to a single slash.
1954         self._lookupDict[''] = self
1955         self._lookupDict['/'] = self
1956         self._lookupDict['//'] = self
1957         self._lookupDict[os.sep] = self
1958         self._lookupDict[os.sep + os.sep] = self
1959
1960     def must_be_same(self, klass):
1961         if klass is Dir:
1962             return
1963         Base.must_be_same(self, klass)
1964
1965     def _lookup_abs(self, p, klass, create=1):
1966         """
1967         Fast (?) lookup of a *normalized* absolute path.
1968
1969         This method is intended for use by internal lookups with
1970         already-normalized path data.  For general-purpose lookups,
1971         use the FS.Entry(), FS.Dir() or FS.File() methods.
1972
1973         The caller is responsible for making sure we're passed a
1974         normalized absolute path; we merely let Python's dictionary look
1975         up and return the One True Node.FS object for the path.
1976
1977         If no Node for the specified "p" doesn't already exist, and
1978         "create" is specified, the Node may be created after recursive
1979         invocation to find or create the parent directory or directories.
1980         """
1981         k = _my_normcase(p)
1982         try:
1983             result = self._lookupDict[k]
1984         except KeyError:
1985             if not create:
1986                 raise SCons.Errors.UserError
1987             # There is no Node for this path name, and we're allowed
1988             # to create it.
1989             dir_name, file_name = os.path.split(p)
1990             dir_node = self._lookup_abs(dir_name, Dir)
1991             result = klass(file_name, dir_node, self.fs)
1992             self._lookupDict[k] = result
1993             dir_node.entries[_my_normcase(file_name)] = result
1994             dir_node.implicit = None
1995
1996             # Double-check on disk (as configured) that the Node we
1997             # created matches whatever is out there in the real world.
1998             result.diskcheck_match()
1999         else:
2000             # There is already a Node for this path name.  Allow it to
2001             # complain if we were looking for an inappropriate type.
2002             result.must_be_same(klass)
2003         return result
2004
2005     def __str__(self):
2006         return self.abspath
2007
2008     def entry_abspath(self, name):
2009         return self.abspath + name
2010
2011     def entry_labspath(self, name):
2012         return '/' + name
2013
2014     def entry_path(self, name):
2015         return self.path + name
2016
2017     def entry_tpath(self, name):
2018         return self.tpath + name
2019
2020     def is_under(self, dir):
2021         if self is dir:
2022             return 1
2023         else:
2024             return 0
2025
2026     def up(self):
2027         return None
2028
2029     def get_dir(self):
2030         return None
2031
2032     def src_builder(self):
2033         return _null
2034
2035 class FileNodeInfo(SCons.Node.NodeInfoBase):
2036     current_version_id = 1
2037
2038     field_list = ['csig', 'timestamp', 'size']
2039
2040     # This should get reset by the FS initialization.
2041     fs = None
2042
2043     def str_to_node(self, s):
2044         top = self.fs.Top
2045         root = top.root
2046         if do_splitdrive:
2047             drive, s = os.path.splitdrive(s)
2048             if drive:
2049                 root = self.fs.get_root(drive)
2050         if not os.path.isabs(s):
2051             s = top.labspath + '/' + s
2052         return root._lookup_abs(s, Entry)
2053
2054 class FileBuildInfo(SCons.Node.BuildInfoBase):
2055     current_version_id = 1
2056
2057     def convert_to_sconsign(self):
2058         """
2059         Converts this FileBuildInfo object for writing to a .sconsign file
2060
2061         This replaces each Node in our various dependency lists with its
2062         usual string representation: relative to the top-level SConstruct
2063         directory, or an absolute path if it's outside.
2064         """
2065         if os.sep == '/':
2066             node_to_str = str
2067         else:
2068             def node_to_str(n):
2069                 try:
2070                     s = n.path
2071                 except AttributeError:
2072                     s = str(n)
2073                 else:
2074                     s = string.replace(s, os.sep, '/')
2075                 return s
2076         for attr in ['bsources', 'bdepends', 'bimplicit']:
2077             try:
2078                 val = getattr(self, attr)
2079             except AttributeError:
2080                 pass
2081             else:
2082                 setattr(self, attr, map(node_to_str, val))
2083     def convert_from_sconsign(self, dir, name):
2084         """
2085         Converts a newly-read FileBuildInfo object for in-SCons use
2086
2087         For normal up-to-date checking, we don't have any conversion to
2088         perform--but we're leaving this method here to make that clear.
2089         """
2090         pass
2091     def prepare_dependencies(self):
2092         """
2093         Prepares a FileBuildInfo object for explaining what changed
2094
2095         The bsources, bdepends and bimplicit lists have all been
2096         stored on disk as paths relative to the top-level SConstruct
2097         directory.  Convert the strings to actual Nodes (for use by the
2098         --debug=explain code and --implicit-cache).
2099         """
2100         attrs = [
2101             ('bsources', 'bsourcesigs'),
2102             ('bdepends', 'bdependsigs'),
2103             ('bimplicit', 'bimplicitsigs'),
2104         ]
2105         for (nattr, sattr) in attrs:
2106             try:
2107                 strings = getattr(self, nattr)
2108                 nodeinfos = getattr(self, sattr)
2109             except AttributeError:
2110                 pass
2111             else:
2112                 nodes = []
2113                 for s, ni in zip(strings, nodeinfos):
2114                     if not isinstance(s, SCons.Node.Node):
2115                         s = ni.str_to_node(s)
2116                     nodes.append(s)
2117                 setattr(self, nattr, nodes)
2118     def format(self, names=0):
2119         result = []
2120         bkids = self.bsources + self.bdepends + self.bimplicit
2121         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
2122         for bkid, bkidsig in zip(bkids, bkidsigs):
2123             result.append(str(bkid) + ': ' +
2124                           string.join(bkidsig.format(names=names), ' '))
2125         result.append('%s [%s]' % (self.bactsig, self.bact))
2126         return string.join(result, '\n')
2127
2128 class File(Base):
2129     """A class for files in a file system.
2130     """
2131
2132     memoizer_counters = []
2133
2134     NodeInfo = FileNodeInfo
2135     BuildInfo = FileBuildInfo
2136
2137     def diskcheck_match(self):
2138         diskcheck_match(self, self.isdir,
2139                         "Directory %s found where file expected.")
2140
2141     def __init__(self, name, directory, fs):
2142         if __debug__: logInstanceCreation(self, 'Node.FS.File')
2143         Base.__init__(self, name, directory, fs)
2144         self._morph()
2145
2146     def Entry(self, name):
2147         """Create an entry node named 'name' relative to
2148         the SConscript directory of this file."""
2149         return self.cwd.Entry(name)
2150
2151     def Dir(self, name, create=True):
2152         """Create a directory node named 'name' relative to
2153         the SConscript directory of this file."""
2154         return self.cwd.Dir(name, create)
2155
2156     def Dirs(self, pathlist):
2157         """Create a list of directories relative to the SConscript
2158         directory of this file."""
2159         return map(lambda p, s=self: s.Dir(p), pathlist)
2160
2161     def File(self, name):
2162         """Create a file node named 'name' relative to
2163         the SConscript directory of this file."""
2164         return self.cwd.File(name)
2165
2166     #def generate_build_dict(self):
2167     #    """Return an appropriate dictionary of values for building
2168     #    this File."""
2169     #    return {'Dir' : self.Dir,
2170     #            'File' : self.File,
2171     #            'RDirs' : self.RDirs}
2172
2173     def _morph(self):
2174         """Turn a file system node into a File object."""
2175         self.scanner_paths = {}
2176         if not hasattr(self, '_local'):
2177             self._local = 0
2178
2179         # If there was already a Builder set on this entry, then
2180         # we need to make sure we call the target-decider function,
2181         # not the source-decider.  Reaching in and doing this by hand
2182         # is a little bogus.  We'd prefer to handle this by adding
2183         # an Entry.builder_set() method that disambiguates like the
2184         # other methods, but that starts running into problems with the
2185         # fragile way we initialize Dir Nodes with their Mkdir builders,
2186         # yet still allow them to be overridden by the user.  Since it's
2187         # not clear right now how to fix that, stick with what works
2188         # until it becomes clear...
2189         if self.has_builder():
2190             self.changed_since_last_build = self.decide_target
2191
2192     def scanner_key(self):
2193         return self.get_suffix()
2194
2195     def get_contents(self):
2196         if not self.rexists():
2197             return ''
2198         fname = self.rfile().abspath
2199         try:
2200             r = open(fname, "rb").read()
2201         except EnvironmentError, e:
2202             if not e.filename:
2203                 e.filename = fname
2204             raise
2205         return r
2206
2207     memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
2208
2209     def get_size(self):
2210         try:
2211             return self._memo['get_size']
2212         except KeyError:
2213             pass
2214
2215         if self.rexists():
2216             size = self.rfile().getsize()
2217         else:
2218             size = 0
2219
2220         self._memo['get_size'] = size
2221
2222         return size
2223
2224     memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp'))
2225
2226     def get_timestamp(self):
2227         try:
2228             return self._memo['get_timestamp']
2229         except KeyError:
2230             pass
2231
2232         if self.rexists():
2233             timestamp = self.rfile().getmtime()
2234         else:
2235             timestamp = 0
2236
2237         self._memo['get_timestamp'] = timestamp
2238
2239         return timestamp
2240
2241     def store_info(self):
2242         # Merge our build information into the already-stored entry.
2243         # This accomodates "chained builds" where a file that's a target
2244         # in one build (SConstruct file) is a source in a different build.
2245         # See test/chained-build.py for the use case.
2246         self.dir.sconsign().store_info(self.name, self)
2247
2248     convert_copy_attrs = [
2249         'bsources',
2250         'bimplicit',
2251         'bdepends',
2252         'bact',
2253         'bactsig',
2254         'ninfo',
2255     ]
2256
2257
2258     convert_sig_attrs = [
2259         'bsourcesigs',
2260         'bimplicitsigs',
2261         'bdependsigs',
2262     ]
2263
2264     def convert_old_entry(self, old_entry):
2265         # Convert a .sconsign entry from before the Big Signature
2266         # Refactoring, doing what we can to convert its information
2267         # to the new .sconsign entry format.
2268         #
2269         # The old format looked essentially like this:
2270         #
2271         #   BuildInfo
2272         #       .ninfo (NodeInfo)
2273         #           .bsig
2274         #           .csig
2275         #           .timestamp
2276         #           .size
2277         #       .bsources
2278         #       .bsourcesigs ("signature" list)
2279         #       .bdepends
2280         #       .bdependsigs ("signature" list)
2281         #       .bimplicit
2282         #       .bimplicitsigs ("signature" list)
2283         #       .bact
2284         #       .bactsig
2285         #
2286         # The new format looks like this:
2287         #
2288         #   .ninfo (NodeInfo)
2289         #       .bsig
2290         #       .csig
2291         #       .timestamp
2292         #       .size
2293         #   .binfo (BuildInfo)
2294         #       .bsources
2295         #       .bsourcesigs (NodeInfo list)
2296         #           .bsig
2297         #           .csig
2298         #           .timestamp
2299         #           .size
2300         #       .bdepends
2301         #       .bdependsigs (NodeInfo list)
2302         #           .bsig
2303         #           .csig
2304         #           .timestamp
2305         #           .size
2306         #       .bimplicit
2307         #       .bimplicitsigs (NodeInfo list)
2308         #           .bsig
2309         #           .csig
2310         #           .timestamp
2311         #           .size
2312         #       .bact
2313         #       .bactsig
2314         #
2315         # The basic idea of the new structure is that a NodeInfo always
2316         # holds all available information about the state of a given Node
2317         # at a certain point in time.  The various .b*sigs lists can just
2318         # be a list of pointers to the .ninfo attributes of the different
2319         # dependent nodes, without any copying of information until it's
2320         # time to pickle it for writing out to a .sconsign file.
2321         #
2322         # The complicating issue is that the *old* format only stored one
2323         # "signature" per dependency, based on however the *last* build
2324         # was configured.  We don't know from just looking at it whether
2325         # it was a build signature, a content signature, or a timestamp
2326         # "signature".  Since we no longer use build signatures, the
2327         # best we can do is look at the length and if it's thirty two,
2328         # assume that it was (or might have been) a content signature.
2329         # If it was actually a build signature, then it will cause a
2330         # rebuild anyway when it doesn't match the new content signature,
2331         # but that's probably the best we can do.
2332         import SCons.SConsign
2333         new_entry = SCons.SConsign.SConsignEntry()
2334         new_entry.binfo = self.new_binfo()
2335         binfo = new_entry.binfo
2336         for attr in self.convert_copy_attrs:
2337             try:
2338                 value = getattr(old_entry, attr)
2339             except AttributeError:
2340                 pass
2341             else:
2342                 setattr(binfo, attr, value)
2343                 delattr(old_entry, attr)
2344         for attr in self.convert_sig_attrs:
2345             try:
2346                 sig_list = getattr(old_entry, attr)
2347             except AttributeError:
2348                 pass
2349             else:
2350                 value = []
2351                 for sig in sig_list:
2352                     ninfo = self.new_ninfo()
2353                     if len(sig) == 32:
2354                         ninfo.csig = sig
2355                     else:
2356                         ninfo.timestamp = sig
2357                     value.append(ninfo)
2358                 setattr(binfo, attr, value)
2359                 delattr(old_entry, attr)
2360         return new_entry
2361
2362     memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
2363
2364     def get_stored_info(self):
2365         try:
2366             return self._memo['get_stored_info']
2367         except KeyError:
2368             pass
2369
2370         try:
2371             sconsign_entry = self.dir.sconsign().get_entry(self.name)
2372         except (KeyError, OSError):
2373             import SCons.SConsign
2374             sconsign_entry = SCons.SConsign.SConsignEntry()
2375             sconsign_entry.binfo = self.new_binfo()
2376             sconsign_entry.ninfo = self.new_ninfo()
2377         else:
2378             if isinstance(sconsign_entry, FileBuildInfo):
2379                 # This is a .sconsign file from before the Big Signature
2380                 # Refactoring; convert it as best we can.
2381                 sconsign_entry = self.convert_old_entry(sconsign_entry)
2382             try:
2383                 delattr(sconsign_entry.ninfo, 'bsig')
2384             except AttributeError:
2385                 pass
2386
2387         self._memo['get_stored_info'] = sconsign_entry
2388
2389         return sconsign_entry
2390
2391     def get_stored_implicit(self):
2392         binfo = self.get_stored_info().binfo
2393         binfo.prepare_dependencies()
2394         try: return binfo.bimplicit
2395         except AttributeError: return None
2396
2397     def rel_path(self, other):
2398         return self.dir.rel_path(other)
2399
2400     def _get_found_includes_key(self, env, scanner, path):
2401         return (id(env), id(scanner), path)
2402
2403     memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
2404
2405     def get_found_includes(self, env, scanner, path):
2406         """Return the included implicit dependencies in this file.
2407         Cache results so we only scan the file once per path
2408         regardless of how many times this information is requested.
2409         """
2410         memo_key = (id(env), id(scanner), path)
2411         try:
2412             memo_dict = self._memo['get_found_includes']
2413         except KeyError:
2414             memo_dict = {}
2415             self._memo['get_found_includes'] = memo_dict
2416         else:
2417             try:
2418                 return memo_dict[memo_key]
2419             except KeyError:
2420                 pass
2421
2422         if scanner:
2423             result = scanner(self, env, path)
2424             result = map(lambda N: N.disambiguate(), result)
2425         else:
2426             result = []
2427
2428         memo_dict[memo_key] = result
2429
2430         return result
2431
2432     def _createDir(self):
2433         # ensure that the directories for this node are
2434         # created.
2435         self.dir._create()
2436
2437     def retrieve_from_cache(self):
2438         """Try to retrieve the node's content from a cache
2439
2440         This method is called from multiple threads in a parallel build,
2441         so only do thread safe stuff here. Do thread unsafe stuff in
2442         built().
2443
2444         Returns true iff the node was successfully retrieved.
2445         """
2446         if self.nocache:
2447             return None
2448         if not self.is_derived():
2449             return None
2450         return self.get_build_env().get_CacheDir().retrieve(self)
2451
2452     def built(self):
2453         """
2454         Called just after this node is successfully built.
2455         """
2456         # Push this file out to cache before the superclass Node.built()
2457         # method has a chance to clear the build signature, which it
2458         # will do if this file has a source scanner.
2459         #
2460         # We have to clear the memoized values *before* we push it to
2461         # cache so that the memoization of the self.exists() return
2462         # value doesn't interfere.
2463         self.clear_memoized_values()
2464         if self.exists():
2465             self.get_build_env().get_CacheDir().push(self)
2466         SCons.Node.Node.built(self)
2467
2468     def visited(self):
2469         if self.exists():
2470             self.get_build_env().get_CacheDir().push_if_forced(self)
2471
2472         ninfo = self.get_ninfo()
2473
2474         csig = self.get_max_drift_csig()
2475         if csig:
2476             ninfo.csig = csig
2477
2478         ninfo.timestamp = self.get_timestamp()
2479         ninfo.size      = self.get_size()
2480
2481         if not self.has_builder():
2482             # This is a source file, but it might have been a target file
2483             # in another build that included more of the DAG.  Copy
2484             # any build information that's stored in the .sconsign file
2485             # into our binfo object so it doesn't get lost.
2486             old = self.get_stored_info()
2487             self.get_binfo().__dict__.update(old.binfo.__dict__)
2488
2489         self.store_info()
2490
2491     def find_src_builder(self):
2492         if self.rexists():
2493             return None
2494         scb = self.dir.src_builder()
2495         if scb is _null:
2496             if diskcheck_sccs(self.dir, self.name):
2497                 scb = get_DefaultSCCSBuilder()
2498             elif diskcheck_rcs(self.dir, self.name):
2499                 scb = get_DefaultRCSBuilder()
2500             else:
2501                 scb = None
2502         if scb is not None:
2503             try:
2504                 b = self.builder
2505             except AttributeError:
2506                 b = None
2507             if b is None:
2508                 self.builder_set(scb)
2509         return scb
2510
2511     def has_src_builder(self):
2512         """Return whether this Node has a source builder or not.
2513
2514         If this Node doesn't have an explicit source code builder, this
2515         is where we figure out, on the fly, if there's a transparent
2516         source code builder for it.
2517
2518         Note that if we found a source builder, we also set the
2519         self.builder attribute, so that all of the methods that actually
2520         *build* this file don't have to do anything different.
2521         """
2522         try:
2523             scb = self.sbuilder
2524         except AttributeError:
2525             scb = self.sbuilder = self.find_src_builder()
2526         return not scb is None
2527
2528     def alter_targets(self):
2529         """Return any corresponding targets in a variant directory.
2530         """
2531         if self.is_derived():
2532             return [], None
2533         return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
2534
2535     def _rmv_existing(self):
2536         self.clear_memoized_values()
2537         e = Unlink(self, [], None)
2538         if isinstance(e, SCons.Errors.BuildError):
2539             raise e
2540
2541     #
2542     # Taskmaster interface subsystem
2543     #
2544
2545     def make_ready(self):
2546         self.has_src_builder()
2547         self.get_binfo()
2548
2549     def prepare(self):
2550         """Prepare for this file to be created."""
2551         SCons.Node.Node.prepare(self)
2552
2553         if self.get_state() != SCons.Node.up_to_date:
2554             if self.exists():
2555                 if self.is_derived() and not self.precious:
2556                     self._rmv_existing()
2557             else:
2558                 try:
2559                     self._createDir()
2560                 except SCons.Errors.StopError, drive:
2561                     desc = "No drive `%s' for target `%s'." % (drive, self)
2562                     raise SCons.Errors.StopError, desc
2563
2564     #
2565     #
2566     #
2567
2568     def remove(self):
2569         """Remove this file."""
2570         if self.exists() or self.islink():
2571             self.fs.unlink(self.path)
2572             return 1
2573         return None
2574
2575     def do_duplicate(self, src):
2576         self._createDir()
2577         Unlink(self, None, None)
2578         e = Link(self, src, None)
2579         if isinstance(e, SCons.Errors.BuildError):
2580             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
2581             raise SCons.Errors.StopError, desc
2582         self.linked = 1
2583         # The Link() action may or may not have actually
2584         # created the file, depending on whether the -n
2585         # option was used or not.  Delete the _exists and
2586         # _rexists attributes so they can be reevaluated.
2587         self.clear()
2588
2589     memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2590
2591     def exists(self):
2592         try:
2593             return self._memo['exists']
2594         except KeyError:
2595             pass
2596         # Duplicate from source path if we are set up to do this.
2597         if self.duplicate and not self.is_derived() and not self.linked:
2598             src = self.srcnode()
2599             if not src is self:
2600                 # At this point, src is meant to be copied in a variant directory.
2601                 src = src.rfile()
2602                 if src.abspath != self.abspath:
2603                     if src.exists():
2604                         self.do_duplicate(src)
2605                         # Can't return 1 here because the duplication might
2606                         # not actually occur if the -n option is being used.
2607                     else:
2608                         # The source file does not exist.  Make sure no old
2609                         # copy remains in the variant directory.
2610                         if Base.exists(self) or self.islink():
2611                             self.fs.unlink(self.path)
2612                         # Return None explicitly because the Base.exists() call
2613                         # above will have cached its value if the file existed.
2614                         self._memo['exists'] = None
2615                         return None
2616         result = Base.exists(self)
2617         self._memo['exists'] = result
2618         return result
2619
2620     #
2621     # SIGNATURE SUBSYSTEM
2622     #
2623
2624     def get_max_drift_csig(self):
2625         """
2626         Returns the content signature currently stored for this node
2627         if it's been unmodified longer than the max_drift value, or the
2628         max_drift value is 0.  Returns None otherwise.
2629         """
2630         old = self.get_stored_info()
2631         mtime = self.get_timestamp()
2632
2633         csig = None
2634         max_drift = self.fs.max_drift
2635         if max_drift > 0:
2636             if (time.time() - mtime) > max_drift:
2637                 try:
2638                     n = old.ninfo
2639                     if n.timestamp and n.csig and n.timestamp == mtime:
2640                         csig = n.csig
2641                 except AttributeError:
2642                     pass
2643         elif max_drift == 0:
2644             try:
2645                 csig = old.ninfo.csig
2646             except AttributeError:
2647                 pass
2648
2649         return csig
2650
2651     def get_csig(self):
2652         """
2653         Generate a node's content signature, the digested signature
2654         of its content.
2655
2656         node - the node
2657         cache - alternate node to use for the signature cache
2658         returns - the content signature
2659         """
2660         ninfo = self.get_ninfo()
2661         try:
2662             return ninfo.csig
2663         except AttributeError:
2664             pass
2665
2666         csig = self.get_max_drift_csig()
2667         if csig is None:
2668
2669             try:
2670                 contents = self.get_contents()
2671             except IOError:
2672                 # This can happen if there's actually a directory on-disk,
2673                 # which can be the case if they've disabled disk checks,
2674                 # or if an action with a File target actually happens to
2675                 # create a same-named directory by mistake.
2676                 csig = ''
2677             else:
2678                 csig = SCons.Util.MD5signature(contents)
2679
2680         ninfo.csig = csig
2681
2682         return csig
2683
2684     #
2685     # DECISION SUBSYSTEM
2686     #
2687
2688     def builder_set(self, builder):
2689         SCons.Node.Node.builder_set(self, builder)
2690         self.changed_since_last_build = self.decide_target
2691
2692     def changed_content(self, target, prev_ni):
2693         cur_csig = self.get_csig()
2694         try:
2695             return cur_csig != prev_ni.csig
2696         except AttributeError:
2697             return 1
2698
2699     def changed_state(self, target, prev_ni):
2700         return (self.state != SCons.Node.up_to_date)
2701
2702     def changed_timestamp_then_content(self, target, prev_ni):
2703         if not self.changed_timestamp_match(target, prev_ni):
2704             try:
2705                 self.get_ninfo().csig = prev_ni.csig
2706             except AttributeError:
2707                 pass
2708             return False
2709         return self.changed_content(target, prev_ni)
2710
2711     def changed_timestamp_newer(self, target, prev_ni):
2712         try:
2713             return self.get_timestamp() > target.get_timestamp()
2714         except AttributeError:
2715             return 1
2716
2717     def changed_timestamp_match(self, target, prev_ni):
2718         try:
2719             return self.get_timestamp() != prev_ni.timestamp
2720         except AttributeError:
2721             return 1
2722
2723     def decide_source(self, target, prev_ni):
2724         return target.get_build_env().decide_source(self, target, prev_ni)
2725
2726     def decide_target(self, target, prev_ni):
2727         return target.get_build_env().decide_target(self, target, prev_ni)
2728
2729     # Initialize this Node's decider function to decide_source() because
2730     # every file is a source file until it has a Builder attached...
2731     changed_since_last_build = decide_source
2732
2733     def is_up_to_date(self):
2734         T = 0
2735         if T: Trace('is_up_to_date(%s):' % self)
2736         if not self.exists():
2737             if T: Trace(' not self.exists():')
2738             # The file doesn't exist locally...
2739             r = self.rfile()
2740             if r != self:
2741                 # ...but there is one in a Repository...
2742                 if not self.changed(r):
2743                     if T: Trace(' changed(%s):' % r)
2744                     # ...and it's even up-to-date...
2745                     if self._local:
2746                         # ...and they'd like a local copy.
2747                         e = LocalCopy(self, r, None)
2748                         if isinstance(e, SCons.Errors.BuildError):
2749                             raise 
2750                         self.store_info()
2751                     if T: Trace(' 1\n')
2752                     return 1
2753             self.changed()
2754             if T: Trace(' None\n')
2755             return None
2756         else:
2757             r = self.changed()
2758             if T: Trace(' self.exists():  %s\n' % r)
2759             return not r
2760
2761     memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
2762
2763     def rfile(self):
2764         try:
2765             return self._memo['rfile']
2766         except KeyError:
2767             pass
2768         result = self
2769         if not self.exists():
2770             norm_name = _my_normcase(self.name)
2771             for dir in self.dir.get_all_rdirs():
2772                 try: node = dir.entries[norm_name]
2773                 except KeyError: node = dir.file_on_disk(self.name)
2774                 if node and node.exists() and \
2775                    (isinstance(node, File) or isinstance(node, Entry) \
2776                     or not node.is_derived()):
2777                         result = node
2778                         break
2779         self._memo['rfile'] = result
2780         return result
2781
2782     def rstr(self):
2783         return str(self.rfile())
2784
2785     def get_cachedir_csig(self):
2786         """
2787         Fetch a Node's content signature for purposes of computing
2788         another Node's cachesig.
2789
2790         This is a wrapper around the normal get_csig() method that handles
2791         the somewhat obscure case of using CacheDir with the -n option.
2792         Any files that don't exist would normally be "built" by fetching
2793         them from the cache, but the normal get_csig() method will try
2794         to open up the local file, which doesn't exist because the -n
2795         option meant we didn't actually pull the file from cachedir.
2796         But since the file *does* actually exist in the cachedir, we
2797         can use its contents for the csig.
2798         """
2799         try:
2800             return self.cachedir_csig
2801         except AttributeError:
2802             pass
2803
2804         cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
2805         if not self.exists() and cachefile and os.path.exists(cachefile):
2806             contents = open(cachefile, 'rb').read()
2807             self.cachedir_csig = SCons.Util.MD5signature(contents)
2808         else:
2809             self.cachedir_csig = self.get_csig()
2810         return self.cachedir_csig
2811
2812     def get_cachedir_bsig(self):
2813         try:
2814             return self.cachesig
2815         except AttributeError:
2816             pass
2817
2818         # Add the path to the cache signature, because multiple
2819         # targets built by the same action will all have the same
2820         # build signature, and we have to differentiate them somehow.
2821         children =  self.children()
2822         sigs = map(lambda n: n.get_cachedir_csig(), children)
2823         executor = self.get_executor()
2824         sigs.append(SCons.Util.MD5signature(executor.get_contents()))
2825         sigs.append(self.path)
2826         self.cachesig = SCons.Util.MD5collect(sigs)
2827         return self.cachesig
2828
2829 default_fs = None
2830
2831 def get_default_fs():
2832     global default_fs
2833     if not default_fs:
2834         default_fs = FS()
2835     return default_fs
2836
2837 class FileFinder:
2838     """
2839     """
2840     if SCons.Memoize.use_memoizer:
2841         __metaclass__ = SCons.Memoize.Memoized_Metaclass
2842
2843     memoizer_counters = []
2844
2845     def __init__(self):
2846         self._memo = {}
2847
2848     def filedir_lookup(self, p, fd=None):
2849         """
2850         A helper method for find_file() that looks up a directory for
2851         a file we're trying to find.  This only creates the Dir Node if
2852         it exists on-disk, since if the directory doesn't exist we know
2853         we won't find any files in it...  :-)
2854
2855         It would be more compact to just use this as a nested function
2856         with a default keyword argument (see the commented-out version
2857         below), but that doesn't work unless you have nested scopes,
2858         so we define it here just so this work under Python 1.5.2.
2859         """
2860         if fd is None:
2861             fd = self.default_filedir
2862         dir, name = os.path.split(fd)
2863         drive, d = os.path.splitdrive(dir)
2864         if d in ('/', os.sep):
2865             return p.fs.get_root(drive).dir_on_disk(name)
2866         if dir:
2867             p = self.filedir_lookup(p, dir)
2868             if not p:
2869                 return None
2870         norm_name = _my_normcase(name)
2871         try:
2872             node = p.entries[norm_name]
2873         except KeyError:
2874             return p.dir_on_disk(name)
2875         # Once we move to Python 2.2 we can do:
2876         #if isinstance(node, (Dir, Entry)):
2877         if isinstance(node, Dir) or isinstance(node, Entry):
2878             return node
2879         return None
2880
2881     def _find_file_key(self, filename, paths, verbose=None):
2882         return (filename, paths)
2883         
2884     memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
2885
2886     def find_file(self, filename, paths, verbose=None):
2887         """
2888         find_file(str, [Dir()]) -> [nodes]
2889
2890         filename - a filename to find
2891         paths - a list of directory path *nodes* to search in.  Can be
2892                 represented as a list, a tuple, or a callable that is
2893                 called with no arguments and returns the list or tuple.
2894
2895         returns - the node created from the found file.
2896
2897         Find a node corresponding to either a derived file or a file
2898         that exists already.
2899
2900         Only the first file found is returned, and none is returned
2901         if no file is found.
2902         """
2903         memo_key = self._find_file_key(filename, paths)
2904         try:
2905             memo_dict = self._memo['find_file']
2906         except KeyError:
2907             memo_dict = {}
2908             self._memo['find_file'] = memo_dict
2909         else:
2910             try:
2911                 return memo_dict[memo_key]
2912             except KeyError:
2913                 pass
2914
2915         if verbose:
2916             if not SCons.Util.is_String(verbose):
2917                 verbose = "find_file"
2918             if not callable(verbose):
2919                 verbose = '  %s: ' % verbose
2920                 verbose = lambda s, v=verbose: sys.stdout.write(v + s)
2921         else:
2922             verbose = lambda x: x
2923
2924         filedir, filename = os.path.split(filename)
2925         if filedir:
2926             # More compact code that we can't use until we drop
2927             # support for Python 1.5.2:
2928             #
2929             #def filedir_lookup(p, fd=filedir):
2930             #    """
2931             #    A helper function that looks up a directory for a file
2932             #    we're trying to find.  This only creates the Dir Node
2933             #    if it exists on-disk, since if the directory doesn't
2934             #    exist we know we won't find any files in it...  :-)
2935             #    """
2936             #    dir, name = os.path.split(fd)
2937             #    if dir:
2938             #        p = filedir_lookup(p, dir)
2939             #        if not p:
2940             #            return None
2941             #    norm_name = _my_normcase(name)
2942             #    try:
2943             #        node = p.entries[norm_name]
2944             #    except KeyError:
2945             #        return p.dir_on_disk(name)
2946             #    # Once we move to Python 2.2 we can do:
2947             #    #if isinstance(node, (Dir, Entry)):
2948             #    if isinstance(node, Dir) or isinstance(node, Entry):
2949             #        return node
2950             #    return None
2951             #paths = filter(None, map(filedir_lookup, paths))
2952
2953             self.default_filedir = filedir
2954             paths = filter(None, map(self.filedir_lookup, paths))
2955
2956         result = None
2957         for dir in paths:
2958             verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
2959             node, d = dir.srcdir_find_file(filename)
2960             if node:
2961                 verbose("... FOUND '%s' in '%s'\n" % (filename, d))
2962                 result = node
2963                 break
2964
2965         memo_dict[memo_key] = result
2966
2967         return result
2968
2969 find_file = FileFinder().find_file