Refactor how actions get executed to eliminate a lot of redundant signature calcualat...
[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 initializes a "default_fs" Node with an FS at the current directory
9 for its own purposes, and for use by scripts or modules looking for the
10 canonical default.
11
12 """
13
14 #
15 # __COPYRIGHT__
16 #
17 # Permission is hereby granted, free of charge, to any person obtaining
18 # a copy of this software and associated documentation files (the
19 # "Software"), to deal in the Software without restriction, including
20 # without limitation the rights to use, copy, modify, merge, publish,
21 # distribute, sublicense, and/or sell copies of the Software, and to
22 # permit persons to whom the Software is furnished to do so, subject to
23 # the following conditions:
24 #
25 # The above copyright notice and this permission notice shall be included
26 # in all copies or substantial portions of the Software.
27 #
28 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
29 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
30 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
33 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 #
36
37 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
38
39 import os
40 import os.path
41 import shutil
42 import stat
43 import string
44
45 import SCons.Action
46 import SCons.Errors
47 import SCons.Node
48 import SCons.Util
49 import SCons.Warnings
50
51 #
52 # SCons.Action objects for interacting with the outside world.
53 #
54 # The Node.FS methods in this module should use these actions to
55 # create and/or remove files and directories; they should *not* use
56 # os.{link,symlink,unlink,mkdir}(), etc., directly.
57 #
58 # Using these SCons.Action objects ensures that descriptions of these
59 # external activities are properly displayed, that the displays are
60 # suppressed when the -s (silent) option is used, and (most importantly)
61 # the actions are disabled when the the -n option is used, in which case
62 # there should be *no* changes to the external file system(s)...
63 #
64
65 if hasattr(os, 'symlink'):
66     def _existsp(p):
67         return os.path.exists(p) or os.path.islink(p)
68 else:
69     _existsp = os.path.exists
70
71 def LinkFunc(target, source, env):
72     src = str(source[0])
73     dest = str(target[0])
74     dir, file = os.path.split(dest)
75     if dir and not os.path.isdir(dir):
76         os.makedirs(dir)
77     # Now actually link the files.  First try to make a hard link.  If that
78     # fails, try a symlink.  If that fails then just copy it.
79     try :
80         os.link(src, dest)
81     except (AttributeError, OSError):
82         try :
83             os.symlink(src, dest)
84         except (AttributeError, OSError):
85             shutil.copy2(src, dest)
86             st=os.stat(src)
87             os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
88     return 0
89
90 Link = SCons.Action.Action(LinkFunc, None)
91
92 def LocalString(target, source, env):
93     return 'Local copy of %s from %s' % (target[0], source[0])
94
95 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
96
97 def UnlinkFunc(target, source, env):
98     os.unlink(target[0].path)
99     return 0
100
101 Unlink = SCons.Action.Action(UnlinkFunc, None)
102
103 def MkdirFunc(target, source, env):
104     os.mkdir(target[0].path)
105     return 0
106
107 Mkdir = SCons.Action.Action(MkdirFunc, None)
108
109 def CacheRetrieveFunc(target, source, env):
110     t = target[0]
111     cachedir, cachefile = t.cachepath()
112     if os.path.exists(cachefile):
113         shutil.copy2(cachefile, t.path)
114         st = os.stat(cachefile)
115         os.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
116         return 0
117     return 1
118
119 def CacheRetrieveString(target, source, env):
120     t = target[0]
121     cachedir, cachefile = t.cachepath()
122     if os.path.exists(cachefile):
123         return "Retrieved `%s' from cache" % t.path
124     return None
125
126 CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
127
128 CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
129
130 def CachePushFunc(target, source, env):
131     t = target[0]
132     cachedir, cachefile = t.cachepath()
133     if os.path.exists(cachefile):
134         # Don't bother copying it if it's already there.
135         return
136
137     if not os.path.isdir(cachedir):
138         os.mkdir(cachedir)
139
140     tempfile = cachefile+'.tmp'
141     try:
142         shutil.copy2(t.path, tempfile)
143         os.rename(tempfile, cachefile)
144         st = os.stat(t.path)
145         os.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
146     except OSError:
147         # It's possible someone else tried writing the file at the same
148         # time we did.  Print a warning but don't stop the build, since
149         # it doesn't affect the correctness of the build.
150         SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning,
151                             "Unable to copy %s to cache. Cache file is %s"
152                                 % (str(target), cachefile))
153         return
154
155 CachePush = SCons.Action.Action(CachePushFunc, None)
156
157 class _Null:
158     pass
159
160 _null = _Null()
161
162 DefaultSCCSBuilder = None
163 DefaultRCSBuilder = None
164
165 def get_DefaultSCCSBuilder():
166     global DefaultSCCSBuilder
167     if DefaultSCCSBuilder is None:
168         import SCons.Builder
169         import SCons.Defaults
170         DefaultSCCSBuilder = SCons.Builder.Builder(action = '$SCCSCOM',
171                                                    env = SCons.Defaults._default_env)
172     return DefaultSCCSBuilder
173
174 def get_DefaultRCSBuilder():
175     global DefaultRCSBuilder
176     if DefaultRCSBuilder is None:
177         import SCons.Builder
178         import SCons.Defaults
179         DefaultRCSBuilder = SCons.Builder.Builder(action = '$RCS_COCOM',
180                                                   env = SCons.Defaults._default_env)
181     return DefaultRCSBuilder
182
183 #
184 class ParentOfRoot:
185     """
186     An instance of this class is used as the parent of the root of a
187     filesystem (POSIX) or drive (Win32). This isn't actually a node,
188     but it looks enough like one so that we don't have to have
189     special purpose code everywhere to deal with dir being None. 
190     This class is an instance of the Null object pattern.
191     """
192     def __init__(self):
193         self.abspath = ''
194         self.path = ''
195         self.abspath_ = ''
196         self.path_ = ''
197         self.name=''
198         self.duplicate=0
199         self.srcdir=None
200         self.build_dirs=[]
201         
202     def is_under(self, dir):
203         return 0
204
205     def up(self):
206         return None
207
208     def getRepositories(self):
209         return []
210
211     def get_dir(self):
212         return None
213
214     def recurse_get_path(self, dir, path_elems):
215         return path_elems
216
217     def src_builder(self):
218         return _null
219
220 if os.path.normcase("TeSt") == os.path.normpath("TeSt"):
221     def _my_normcase(x):
222         return x
223 else:
224     def _my_normcase(x):
225         return string.upper(x)
226
227 class EntryProxy(SCons.Util.Proxy):
228     def __init__(self, entry):
229         SCons.Util.Proxy.__init__(self, entry)
230         self.abspath = SCons.Util.SpecialAttrWrapper(entry.abspath,
231                                                      entry.name + "_abspath")
232         filebase, suffix = os.path.splitext(entry.name)
233         self.filebase = SCons.Util.SpecialAttrWrapper(filebase,
234                                                       entry.name + "_filebase")
235         self.suffix = SCons.Util.SpecialAttrWrapper(suffix,
236                                                     entry.name + "_suffix")
237         self.file = SCons.Util.SpecialAttrWrapper(entry.name,
238                                                   entry.name + "_file")
239
240     def __get_base_path(self):
241         """Return the file's directory and file name, with the
242         suffix stripped."""
243         return SCons.Util.SpecialAttrWrapper(os.path.splitext(self.get().get_path())[0],
244                                              self.get().name + "_base")
245
246     def __get_posix_path(self):
247         """Return the path with / as the path separator, regardless
248         of platform."""
249         if os.sep == '/':
250             return self
251         else:
252             return SCons.Util.SpecialAttrWrapper(string.replace(self.get().get_path(),
253                                                                 os.sep, '/'),
254                                                  self.get().name + "_posix")
255
256     def __get_srcnode(self):
257         return EntryProxy(self.get().srcnode())
258
259     def __get_srcdir(self):
260         """Returns the directory containing the source node linked to this
261         node via BuildDir(), or the directory of this node if not linked."""
262         return EntryProxy(self.get().srcnode().dir)
263
264     def __get_dir(self):
265         return EntryProxy(self.get().dir)
266
267     dictSpecialAttrs = { "base" : __get_base_path,
268                          "posix" : __get_posix_path,
269                          "srcpath" : __get_srcnode,
270                          "srcdir" : __get_srcdir,
271                          "dir" : __get_dir }
272
273     def __getattr__(self, name):
274         # This is how we implement the "special" attributes
275         # such as base, posix, srcdir, etc.
276         try:
277             return self.dictSpecialAttrs[name](self)
278         except KeyError:
279             return SCons.Util.Proxy.__getattr__(self, name)
280         
281
282 class Entry(SCons.Node.Node):
283     """A generic class for file system entries.  This class is for
284     when we don't know yet whether the entry being looked up is a file
285     or a directory.  Instances of this class can morph into either
286     Dir or File objects by a later, more precise lookup.
287
288     Note: this class does not define __cmp__ and __hash__ for efficiency
289     reasons.  SCons does a lot of comparing of Entry objects, and so that
290     operation must be as fast as possible, which means we want to use
291     Python's built-in object identity comparison.
292     """
293
294     def __init__(self, name, directory, fs):
295         """Initialize a generic file system Entry.
296         
297         Call the superclass initialization, take care of setting up
298         our relative and absolute paths, identify our parent
299         directory, and indicate that this node should use
300         signatures."""
301         SCons.Node.Node.__init__(self)
302
303         self.name = name
304         self.fs = fs
305         self.relpath = {}
306
307         assert directory, "A directory must be provided"
308
309         self.abspath = directory.abspath_ + name
310         if directory.path == '.':
311             self.path = name
312         else:
313             self.path = directory.path_ + name
314
315         self.path_ = self.path
316         self.abspath_ = self.abspath
317         self.dir = directory
318         self.cwd = None # will hold the SConscript directory for target nodes
319         self.duplicate = directory.duplicate
320
321     def clear(self):
322         """Completely clear an Entry of all its cached state (so that it
323         can be re-evaluated by interfaces that do continuous integration
324         builds).
325         """
326         SCons.Node.Node.clear(self)
327         try:
328             delattr(self, '_exists')
329         except AttributeError:
330             pass
331         try:
332             delattr(self, '_rexists')
333         except AttributeError:
334             pass
335
336     def get_dir(self):
337         return self.dir
338
339     def __str__(self):
340         """A FS node's string representation is its path name."""
341         if self.duplicate or self.has_builder():
342             return self.get_path()
343         return self.srcnode().get_path()
344
345     def get_contents(self):
346         """Fetch the contents of the entry.
347         
348         Since this should return the real contents from the file
349         system, we check to see into what sort of subclass we should
350         morph this Entry."""
351         if os.path.isfile(self.abspath):
352             self.__class__ = File
353             self._morph()
354             return File.get_contents(self)
355         if os.path.isdir(self.abspath):
356             self.__class__ = Dir
357             self._morph()
358             return Dir.get_contents(self)
359         raise AttributeError
360
361     def exists(self):
362         try:
363             return self._exists
364         except AttributeError:
365             self._exists = _existsp(self.abspath)
366             return self._exists
367
368     def rexists(self):
369         if not hasattr(self, '_rexists'):
370             self._rexists = self.rfile().exists()
371         return self._rexists
372
373     def get_parents(self):
374         parents = SCons.Node.Node.get_parents(self)
375         if self.dir and not isinstance(self.dir, ParentOfRoot):
376             parents.append(self.dir)
377         return parents
378
379     def current(self, calc):
380         """If the underlying path doesn't exist, we know the node is
381         not current without even checking the signature, so return 0.
382         Otherwise, return None to indicate that signature calculation
383         should proceed as normal to find out if the node is current."""
384         bsig = calc.bsig(self)
385         if not self.exists():
386             return 0
387         return calc.current(self, bsig)
388
389     def is_under(self, dir):
390         if self is dir:
391             return 1
392         else:
393             return self.dir.is_under(dir)
394
395     def set_local(self):
396         self._local = 1
397
398     def srcnode(self):
399         """If this node is in a build path, return the node
400         corresponding to its source file.  Otherwise, return
401         ourself."""
402         try:
403             return self._srcnode
404         except AttributeError:
405             dir=self.dir
406             name=self.name
407             while dir:
408                 if dir.srcdir:
409                     self._srcnode = self.fs.Entry(name, dir.srcdir,
410                                                   klass=self.__class__)
411                     return self._srcnode
412                 name = dir.name + os.sep + name
413                 dir=dir.get_dir()
414             self._srcnode = self
415             return self._srcnode
416
417     def recurse_get_path(self, dir, path_elems):
418         """Recursively build a path relative to a supplied directory
419         node."""
420         if self != dir:
421             path_elems.append(self.name)
422             path_elems = self.dir.recurse_get_path(dir, path_elems)
423         return path_elems
424
425     def get_path(self, dir=None):
426         """Return path relative to the current working directory of the
427         FS object that owns us."""
428         if not dir:
429             dir = self.fs.getcwd()
430         try:
431             return self.relpath[dir]
432         except KeyError:
433             if self == dir:
434                 # Special case, return "." as the path
435                 ret = '.'
436             else:
437                 path_elems = self.recurse_get_path(dir, [])
438                 path_elems.reverse()
439                 ret = string.join(path_elems, os.sep)
440             self.relpath[dir] = ret
441             return ret
442             
443     def set_src_builder(self, builder):
444         """Set the source code builder for this node."""
445         self.sbuilder = builder
446
447     def src_builder(self):
448         """Fetch the source code builder for this node.
449
450         If there isn't one, we cache the source code builder specified
451         for the directory (which in turn will cache the value from its
452         parent directory, and so on up to the file system root).
453         """
454         try:
455             scb = self.sbuilder
456         except AttributeError:
457             scb = self.dir.src_builder()
458             self.sbuilder = scb
459         return scb
460
461     def get_abspath(self):
462         """Get the absolute path of the file."""
463         return self.abspath
464
465     def for_signature(self):
466         # Return just our name.  Even an absolute path would not work,
467         # because that can change thanks to symlinks or remapped network
468         # paths.
469         return self.name
470
471     def get_subst_proxy(self):
472         try:
473             return self._proxy
474         except AttributeError:
475             ret = EntryProxy(self)
476             self._proxy = ret
477             return ret
478
479 # This is for later so we can differentiate between Entry the class and Entry
480 # the method of the FS class.
481 _classEntry = Entry
482
483
484 class FS:
485     def __init__(self, path = None):
486         """Initialize the Node.FS subsystem.
487
488         The supplied path is the top of the source tree, where we
489         expect to find the top-level build file.  If no path is
490         supplied, the current directory is the default.
491
492         The path argument must be a valid absolute path.
493         """
494         if path == None:
495             self.pathTop = os.getcwd()
496         else:
497             self.pathTop = path
498         self.Root = {}
499         self.Top = None
500         self.SConstruct_dir = None
501         self.CachePath = None
502         self.cache_force = None
503         self.cache_show = None
504
505     def set_toplevel_dir(self, path):
506         assert not self.Top, "You can only set the top-level path on an FS object that has not had its File, Dir, or Entry methods called yet."
507         self.pathTop = path
508
509     def set_SConstruct_dir(self, dir):
510         self.SConstruct_dir = dir
511         
512     def __setTopLevelDir(self):
513         if not self.Top:
514             self.Top = self.__doLookup(Dir, os.path.normpath(self.pathTop))
515             self.Top.path = '.'
516             self.Top.path_ = '.' + os.sep
517             self._cwd = self.Top
518         
519     def getcwd(self):
520         self.__setTopLevelDir()
521         return self._cwd
522
523     def __checkClass(self, node, klass):
524         if klass == Entry:
525             return node
526         if node.__class__ == Entry:
527             node.__class__ = klass
528             node._morph()
529             return node
530         if not isinstance(node, klass):
531             raise TypeError, "Tried to lookup %s '%s' as a %s." % \
532                   (node.__class__.__name__, node.path, klass.__name__)
533         return node
534         
535     def __doLookup(self, fsclass, name, directory = None, create = 1):
536         """This method differs from the File and Dir factory methods in
537         one important way: the meaning of the directory parameter.
538         In this method, if directory is None or not supplied, the supplied
539         name is expected to be an absolute path.  If you try to look up a
540         relative path with directory=None, then an AssertionError will be
541         raised."""
542
543         if not name:
544             # This is a stupid hack to compensate for the fact
545             # that the POSIX and Win32 versions of os.path.normpath()
546             # behave differently.  In particular, in POSIX:
547             #   os.path.normpath('./') == '.'
548             # in Win32
549             #   os.path.normpath('./') == ''
550             #   os.path.normpath('.\\') == ''
551             #
552             # This is a definite bug in the Python library, but we have
553             # to live with it.
554             name = '.'
555         path_comp = string.split(name, os.sep)
556         drive, path_first = os.path.splitdrive(path_comp[0])
557         if not path_first:
558             # Absolute path
559             drive = _my_normcase(drive)
560             try:
561                 directory = self.Root[drive]
562             except KeyError:
563                 if not create:
564                     raise SCons.Errors.UserError
565                 dir = Dir(drive, ParentOfRoot(), self)
566                 dir.path = dir.path + os.sep
567                 dir.abspath = dir.abspath + os.sep
568                 self.Root[drive] = dir
569                 directory = dir
570             path_comp = path_comp[1:]
571         else:
572             path_comp = [ path_first, ] + path_comp[1:]
573             
574         # Lookup the directory
575         for path_name in path_comp[:-1]:
576             path_norm = _my_normcase(path_name)
577             try:
578                 directory = self.__checkClass(directory.entries[path_norm],
579                                               Dir)
580             except KeyError:
581                 if not create:
582                     raise SCons.Errors.UserError
583
584                 # look at the actual filesystem and make sure there isn't
585                 # a file already there
586                 path = directory.path_ + path_name
587                 if os.path.isfile(path):
588                     raise TypeError, \
589                           "File %s found where directory expected." % path
590
591                 dir_temp = Dir(path_name, directory, self)
592                 directory.entries[path_norm] = dir_temp
593                 directory.add_wkid(dir_temp)
594                 directory = dir_temp
595         file_name = _my_normcase(path_comp[-1])
596         try:
597             ret = self.__checkClass(directory.entries[file_name], fsclass)
598         except KeyError:
599             if not create:
600                 raise SCons.Errors.UserError
601
602             # make sure we don't create File nodes when there is actually
603             # a directory at that path on the disk, and vice versa
604             path = directory.path_ + path_comp[-1]
605             if fsclass == File:
606                 if os.path.isdir(path):
607                     raise TypeError, \
608                           "Directory %s found where file expected." % path
609             elif fsclass == Dir:
610                 if os.path.isfile(path):
611                     raise TypeError, \
612                           "File %s found where directory expected." % path
613             
614             ret = fsclass(path_comp[-1], directory, self)
615             directory.entries[file_name] = ret
616             directory.add_wkid(ret)
617         return ret
618
619     def __transformPath(self, name, directory):
620         """Take care of setting up the correct top-level directory,
621         usually in preparation for a call to doLookup().
622
623         If the path name is prepended with a '#', then it is unconditionally
624         interpreted as relative to the top-level directory of this FS.
625
626         If directory is None, and name is a relative path,
627         then the same applies.
628         """
629         self.__setTopLevelDir()
630         if name and name[0] == '#':
631             directory = self.Top
632             name = name[1:]
633             if name and (name[0] == os.sep or name[0] == '/'):
634                 # Correct such that '#/foo' is equivalent
635                 # to '#foo'.
636                 name = name[1:]
637             name = os.path.join('.', os.path.normpath(name))
638         elif not directory:
639             directory = self._cwd
640         return (os.path.normpath(name), directory)
641
642     def chdir(self, dir, change_os_dir=0):
643         """Change the current working directory for lookups.
644         If change_os_dir is true, we will also change the "real" cwd
645         to match.
646         """
647         self.__setTopLevelDir()
648         curr=self._cwd
649         try:
650             if not dir is None:
651                 self._cwd = dir
652                 if change_os_dir:
653                     os.chdir(dir.abspath)
654         except:
655             self._cwd = curr
656             raise
657
658     def Entry(self, name, directory = None, create = 1, klass=None):
659         """Lookup or create a generic Entry node with the specified name.
660         If the name is a relative path (begins with ./, ../, or a file
661         name), then it is looked up relative to the supplied directory
662         node, or to the top level directory of the FS (supplied at
663         construction time) if no directory is supplied.
664         """
665
666         if not klass:
667             klass = Entry
668
669         if isinstance(name, Entry):
670             return self.__checkClass(name, klass)
671         else:
672             if directory and not isinstance(directory, Dir):
673                 directory = self.Dir(directory)
674             name, directory = self.__transformPath(name, directory)
675             return self.__doLookup(klass, name, directory, create)
676     
677     def File(self, name, directory = None, create = 1):
678         """Lookup or create a File node with the specified name.  If
679         the name is a relative path (begins with ./, ../, or a file name),
680         then it is looked up relative to the supplied directory node,
681         or to the top level directory of the FS (supplied at construction
682         time) if no directory is supplied.
683
684         This method will raise TypeError if a directory is found at the
685         specified path.
686         """
687
688         return self.Entry(name, directory, create, File)
689     
690     def Dir(self, name, directory = None, create = 1):
691         """Lookup or create a Dir node with the specified name.  If
692         the name is a relative path (begins with ./, ../, or a file name),
693         then it is looked up relative to the supplied directory node,
694         or to the top level directory of the FS (supplied at construction
695         time) if no directory is supplied.
696
697         This method will raise TypeError if a normal file is found at the
698         specified path.
699         """
700
701         return self.Entry(name, directory, create, Dir)
702     
703     def BuildDir(self, build_dir, src_dir, duplicate=1):
704         """Link the supplied build directory to the source directory
705         for purposes of building files."""
706         
707         self.__setTopLevelDir()
708         if not isinstance(src_dir, SCons.Node.Node):
709             src_dir = self.Dir(src_dir)
710         if not isinstance(build_dir, SCons.Node.Node):
711             build_dir = self.Dir(build_dir)
712         if not src_dir.is_under(self.Top):
713             raise SCons.Errors.UserError, "Source directory must be under top of build tree."
714         if src_dir.is_under(build_dir):
715             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
716         build_dir.link(src_dir, duplicate)
717
718     def Repository(self, *dirs):
719         """Specify Repository directories to search."""
720         for d in dirs:
721             if not isinstance(d, SCons.Node.Node):
722                 d = self.Dir(d)
723             self.__setTopLevelDir()
724             self.Top.addRepository(d)
725
726     def Rsearch(self, path, clazz=_classEntry, cwd=None):
727         """Search for something in a Repository.  Returns the first
728         one found in the list, or None if there isn't one."""
729         if isinstance(path, SCons.Node.Node):
730             return path
731         else:
732             name, d = self.__transformPath(path, cwd)
733             n = self.__doLookup(clazz, name, d)
734             if n.exists():
735                 return n
736             if isinstance(n, Dir):
737                 # If n is a Directory that has Repositories directly
738                 # attached to it, then any of those is a valid Repository
739                 # path.  Return the first one that exists.
740                 reps = filter(lambda x: x.exists(), n.getRepositories())
741                 if len(reps):
742                     return reps[0]
743             d = n.get_dir()
744             name = n.name
745             # Search repositories of all directories that this file is under.
746             while d:
747                 for rep in d.getRepositories():
748                     try:
749                         rnode = self.__doLookup(clazz, name, rep)
750                         # Only find the node if it exists and it is not
751                         # a derived file.  If for some reason, we are
752                         # explicitly building a file IN a Repository, we
753                         # don't want it to show up in the build tree.
754                         # This is usually the case with BuildDir().
755                         # We only want to find pre-existing files.
756                         if rnode.exists() and \
757                            (isinstance(rnode, Dir) or not rnode.has_builder()):
758                             return rnode
759                     except TypeError:
760                         pass # Wrong type of node.
761                 # Prepend directory name
762                 name = d.name + os.sep + name
763                 # Go up one directory
764                 d = d.get_dir()
765         return None
766
767     def Rsearchall(self, pathlist, must_exist=1, clazz=_classEntry, cwd=None):
768         """Search for a list of somethings in the Repository list."""
769         ret = []
770         if SCons.Util.is_String(pathlist):
771             pathlist = string.split(pathlist, os.pathsep)
772         if not SCons.Util.is_List(pathlist):
773             pathlist = [pathlist]
774         for path in pathlist:
775             if isinstance(path, SCons.Node.Node):
776                 ret.append(path)
777             else:
778                 name, d = self.__transformPath(path, cwd)
779                 n = self.__doLookup(clazz, name, d)
780                 if not must_exist or n.exists():
781                     ret.append(n)
782                 if isinstance(n, Dir):
783                     # If this node is a directory, then any repositories
784                     # attached to this node can be repository paths.
785                     ret.extend(filter(lambda x, me=must_exist, clazz=clazz: isinstance(x, clazz) and (not me or x.exists()),
786                                       n.getRepositories()))
787                     
788                 d = n.get_dir()
789                 name = n.name
790                 # Search repositories of all directories that this file
791                 # is under.
792                 while d:
793                     for rep in d.getRepositories():
794                         try:
795                             rnode = self.__doLookup(clazz, name, rep)
796                             # Only find the node if it exists (or
797                             # must_exist is zero) and it is not a
798                             # derived file.  If for some reason, we
799                             # are explicitly building a file IN a
800                             # Repository, we don't want it to show up in
801                             # the build tree.  This is usually the case
802                             # with BuildDir().  We only want to find
803                             # pre-existing files.
804                             if (not must_exist or rnode.exists()) and \
805                                (not rnode.has_builder() or isinstance(rnode, Dir)):
806                                 ret.append(rnode)
807                         except TypeError:
808                             pass # Wrong type of node.
809                     # Prepend directory name
810                     name = d.name + os.sep + name
811                     # Go up one directory
812                     d = d.get_dir()
813         return ret
814
815     def CacheDir(self, path):
816         self.CachePath = path
817
818     def build_dir_target_climb(self, dir, tail):
819         """Create targets in corresponding build directories
820
821         Climb the directory tree, and look up path names
822         relative to any linked build directories we find.
823         """
824         targets = []
825         message = None
826         while dir:
827             for bd in dir.build_dirs:
828                 p = apply(os.path.join, [bd.path] + tail)
829                 targets.append(self.Entry(p))
830             tail = [dir.name] + tail
831             dir = dir.up()
832         if targets:
833             message = "building associated BuildDir targets: %s" % string.join(map(str, targets))
834         return targets, message
835
836 # XXX TODO?
837 # Annotate with the creator
838 # rel_path
839 # linked_targets
840 # is_accessible
841
842 class Dir(Entry):
843     """A class for directories in a file system.
844     """
845
846     def __init__(self, name, directory, fs):
847         Entry.__init__(self, name, directory, fs)
848         self._morph()
849
850     def _morph(self):
851         """Turn a file system node (either a freshly initialized
852         directory object or a separate Entry object) into a
853         proper directory object.
854         
855         Modify our paths to add the trailing slash that indicates
856         a directory.  Set up this directory's entries and hook it
857         into the file system tree.  Specify that directories (this
858         node) don't use signatures for currency calculation."""
859
860         self.path_ = self.path + os.sep
861         self.abspath_ = self.abspath + os.sep
862         self.repositories = []
863         self.srcdir = None
864         
865         self.entries = {}
866         self.entries['.'] = self
867         self.entries['..'] = self.dir
868         self.cwd = self
869         self.builder = 1
870         self._sconsign = None
871         self.build_dirs = []
872         
873     def __clearRepositoryCache(self, duplicate=None):
874         """Called when we change the repository(ies) for a directory.
875         This clears any cached information that is invalidated by changing
876         the repository."""
877
878         for node in self.entries.values():
879             if node != self.dir:
880                 if node != self and isinstance(node, Dir):
881                     node.__clearRepositoryCache(duplicate)
882                 else:
883                     try:
884                         del node._srcreps
885                     except AttributeError:
886                         pass
887                     try:
888                         del node._rfile
889                     except AttributeError:
890                         pass
891                     try:
892                         del node._rexists
893                     except AttributeError:
894                         pass
895                     try:
896                         del node._exists
897                     except AttributeError:
898                         pass
899                     try:
900                         del node._srcnode
901                     except AttributeError:
902                         pass
903                     if duplicate != None:
904                         node.duplicate=duplicate
905     
906     def __resetDuplicate(self, node):
907         if node != self:
908             node.duplicate = node.get_dir().duplicate
909
910     def Entry(self, name):
911         """Create an entry node named 'name' relative to this directory."""
912         return self.fs.Entry(name, self)
913
914     def Dir(self, name):
915         """Create a directory node named 'name' relative to this directory."""
916         return self.fs.Dir(name, self)
917
918     def File(self, name):
919         """Create a file node named 'name' relative to this directory."""
920         return self.fs.File(name, self)
921
922     def link(self, srcdir, duplicate):
923         """Set this directory as the build directory for the
924         supplied source directory."""
925         self.srcdir = srcdir
926         self.duplicate = duplicate
927         self.__clearRepositoryCache(duplicate)
928         srcdir.build_dirs.append(self)
929
930     def getRepositories(self):
931         """Returns a list of repositories for this directory."""
932         if self.srcdir and not self.duplicate:
933             try:
934                 return self._srcreps
935             except AttributeError:
936                 self._srcreps = self.fs.Rsearchall(self.srcdir.path,
937                                                    clazz=Dir,
938                                                    must_exist=0,
939                                                    cwd=self.fs.Top) \
940                                 + self.repositories
941                 return self._srcreps
942         return self.repositories
943
944     def addRepository(self, dir):
945         if not dir in self.repositories and dir != self:
946             self.repositories.append(dir)
947             self.__clearRepositoryCache()
948
949     def up(self):
950         return self.entries['..']
951
952     def root(self):
953         if not self.entries['..']:
954             return self
955         else:
956             return self.entries['..'].root()
957
958     def all_children(self, scan):
959         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
960         kids = map(lambda x, s=self: s.entries[x], keys)
961         def c(one, two):
962             if one.abspath < two.abspath:
963                return -1
964             if one.abspath > two.abspath:
965                return 1
966             return 0
967         kids.sort(c)
968         return kids + SCons.Node.Node.all_children(self, 0)
969
970     def get_actions(self):
971         """A null "builder" for directories."""
972         return []
973
974     def build(self):
975         """A null "builder" for directories."""
976         pass
977
978     def alter_targets(self):
979         """Return any corresponding targets in a build directory.
980         """
981         return self.fs.build_dir_target_climb(self, [])
982
983     def calc_signature(self, calc):
984         """A directory has no signature."""
985         return None
986
987     def set_bsig(self, bsig):
988         """A directory has no signature."""
989         bsig = None
990
991     def set_csig(self, csig):
992         """A directory has no signature."""
993         csig = None
994
995     def get_contents(self):
996         """Return a fixed "contents" value of a directory."""
997         return ''
998
999     def prepare(self):
1000         pass
1001
1002     def current(self, calc):
1003         """If all of our children were up-to-date, then this
1004         directory was up-to-date, too."""
1005         state = 0
1006         for kid in self.children(None):
1007             s = kid.get_state()
1008             if s and (not state or s > state):
1009                 state = s
1010         import SCons.Node
1011         if state == 0 or state == SCons.Node.up_to_date:
1012             return 1
1013         else:
1014             return 0
1015
1016     def rdir(self):
1017         try:
1018             return self._rdir
1019         except AttributeError:
1020             self._rdir = self
1021             if not self.exists():
1022                 n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
1023                 if n:
1024                     self._rdir = n
1025             return self._rdir
1026
1027     def sconsign(self):
1028         """Return the .sconsign file info for this directory,
1029         creating it first if necessary."""
1030         if not self._sconsign:
1031             import SCons.Sig
1032             self._sconsign = SCons.Sig.SConsignFile(self)
1033         return self._sconsign
1034
1035     def srcnode(self):
1036         """Dir has a special need for srcnode()...if we
1037         have a srcdir attribute set, then that *is* our srcnode."""
1038         if self.srcdir:
1039             return self.srcdir
1040         return Entry.srcnode(self)
1041
1042 # XXX TODO?
1043 # base_suf
1044 # suffix
1045 # addsuffix
1046 # accessible
1047 # relpath
1048
1049 class File(Entry):
1050     """A class for files in a file system.
1051     """
1052     def __init__(self, name, directory, fs):
1053         Entry.__init__(self, name, directory, fs)
1054         self._morph()
1055
1056     def Entry(self, name):
1057         """Create an entry node named 'name' relative to
1058         the SConscript directory of this file."""
1059         return self.fs.Entry(name, self.cwd)
1060
1061     def Dir(self, name):
1062         """Create a directory node named 'name' relative to
1063         the SConscript directory of this file."""
1064         return self.fs.Dir(name, self.cwd)
1065
1066     def File(self, name):
1067         """Create a file node named 'name' relative to
1068         the SConscript directory of this file."""
1069         return self.fs.File(name, self.cwd)
1070
1071     def RDirs(self, pathlist):
1072         """Search for a list of directories in the Repository list."""
1073         return self.fs.Rsearchall(pathlist, clazz=Dir, must_exist=0,
1074                                   cwd=self.cwd)
1075     
1076     def generate_build_env(self, env):
1077         """Generate an appropriate Environment to build this File."""
1078         return env.Override({'Dir' : self.Dir,
1079                              'File' : self.File,
1080                              'RDirs' : self.RDirs})
1081         
1082     def _morph(self):
1083         """Turn a file system node into a File object."""
1084         self.scanner_paths = {}
1085         self.found_includes = {}
1086         if not hasattr(self, '_local'):
1087             self._local = 0
1088
1089     def root(self):
1090         return self.dir.root()
1091
1092     def get_contents(self):
1093         if not self.rexists():
1094             return ''
1095         return open(self.rfile().abspath, "rb").read()
1096
1097     def get_timestamp(self):
1098         if self.rexists():
1099             return os.path.getmtime(self.rfile().abspath)
1100         else:
1101             return 0
1102
1103     def calc_signature(self, calc, cache=None):
1104         """
1105         Select and calculate the appropriate build signature for a File.
1106
1107         self - the File node
1108         calc - the signature calculation module
1109         cache - alternate node to use for the signature cache
1110         returns - the signature
1111         """
1112
1113         if self.has_builder():
1114             if SCons.Sig.build_signature:
1115                 return calc.bsig(self.rfile(), self)
1116             else:
1117                 return calc.csig(self.rfile(), self)
1118         elif not self.rexists():
1119             return None
1120         else:
1121             return calc.csig(self.rfile(), self)
1122         
1123     def store_csig(self):
1124         self.dir.sconsign().set_csig(self.name, self.get_csig())
1125
1126     def store_bsig(self):
1127         self.dir.sconsign().set_bsig(self.name, self.get_bsig())
1128
1129     def store_implicit(self):
1130         self.dir.sconsign().set_implicit(self.name, self.implicit)
1131
1132     def store_timestamp(self):
1133         self.dir.sconsign().set_timestamp(self.name, self.get_timestamp())
1134
1135     def get_prevsiginfo(self):
1136         """Fetch the previous signature information from the
1137         .sconsign entry."""
1138         return self.dir.sconsign().get(self.name)
1139
1140     def get_stored_implicit(self):
1141         return self.dir.sconsign().get_implicit(self.name)
1142
1143     def get_found_includes(self, env, scanner, target):
1144         """Return the included implicit dependencies in this file.
1145         Cache results so we only scan the file once regardless of
1146         how many times this information is requested."""
1147         if not scanner:
1148             return []
1149
1150         try:
1151             path = target.scanner_paths[scanner]
1152         except AttributeError:
1153             # The target had no scanner_paths attribute, which means
1154             # it's an Alias or some other node that's not actually a
1155             # file.  In that case, back off and use the path for this
1156             # node itself.
1157             try:
1158                 path = self.scanner_paths[scanner]
1159             except KeyError:
1160                 path = scanner.path(env, self.cwd)
1161                 self.scanner_paths[scanner] = path
1162         except KeyError:
1163             path = scanner.path(env, target.cwd)
1164             target.scanner_paths[scanner] = path
1165
1166         try:
1167             includes = self.found_includes[path]
1168         except KeyError:
1169             includes = scanner(self, env, path)
1170             self.found_includes[path] = includes
1171
1172         return includes
1173
1174     def scanner_key(self):
1175         return os.path.splitext(self.name)[1]
1176
1177     def _createDir(self):
1178         # ensure that the directories for this node are
1179         # created.
1180
1181         listDirs = []
1182         parent=self.dir
1183         while parent:
1184             if parent.exists():
1185                 break
1186             listDirs.append(parent)
1187             p = parent.up()
1188             if isinstance(p, ParentOfRoot):
1189                 raise SCons.Errors.StopError, parent.path
1190             parent = p
1191         listDirs.reverse()
1192         for dirnode in listDirs:
1193             try:
1194                 Mkdir(dirnode, None, None)
1195                 # The Mkdir() action may or may not have actually
1196                 # created the directory, depending on whether the -n
1197                 # option was used or not.  Delete the _exists and
1198                 # _rexists attributes so they can be reevaluated.
1199                 if hasattr(dirnode, '_exists'):
1200                     delattr(dirnode, '_exists')
1201                 if hasattr(dirnode, '_rexists'):
1202                     delattr(dirnode, '_rexists')
1203             except OSError:
1204                 pass
1205
1206     def build(self):
1207         """Actually build the file.
1208
1209         This overrides the base class build() method to check for the
1210         existence of derived files in a CacheDir before going ahead and
1211         building them.
1212
1213         This method is called from multiple threads in a parallel build,
1214         so only do thread safe stuff here. Do thread unsafe stuff in
1215         built().
1216         """
1217         b = self.has_builder()
1218         if not b and not self.has_src_builder():
1219             return
1220         if b and self.fs.CachePath:
1221             if self.fs.cache_show:
1222                 if CacheRetrieveSilent(self, None, None) == 0:
1223                     def do_print(action, targets, sources, env, self=self):
1224                         al = action.strfunction(targets, self.sources, env)
1225                         if not SCons.Util.is_List(al):
1226                             al = [al]
1227                         for a in al:
1228                             action.show(a)
1229                     self._for_each_action(do_print)
1230                     return
1231             elif CacheRetrieve(self, None, None) == 0:
1232                 return
1233         SCons.Node.Node.build(self)
1234
1235     def built(self):
1236         """Called just after this node is sucessfully built."""
1237         # Push this file out to cache before the superclass Node.built()
1238         # method has a chance to clear the build signature, which it
1239         # will do if this file has a source scanner.
1240         if self.fs.CachePath and os.path.exists(self.path):
1241             CachePush(self, None, None)
1242         SCons.Node.Node.built(self)
1243         self.found_includes = {}
1244         if hasattr(self, '_exists'):
1245             delattr(self, '_exists')
1246         if hasattr(self, '_rexists'):
1247             delattr(self, '_rexists')
1248
1249     def visited(self):
1250         if self.fs.CachePath and self.fs.cache_force and os.path.exists(self.path):
1251             CachePush(self, None, None)
1252
1253     def has_src_builder(self):
1254         """Return whether this Node has a source builder or not.
1255
1256         If this Node doesn't have an explicit source code builder, this
1257         is where we figure out, on the fly, if there's a transparent
1258         source code builder for it.
1259
1260         Note that if we found a source builder, we also set the
1261         self.builder attribute, so that all of the methods that actually
1262         *build* this file don't have to do anything different.
1263         """
1264         try:
1265             scb = self.sbuilder
1266         except AttributeError:
1267             if self.rexists():
1268                 scb = None
1269             else:
1270                 scb = self.dir.src_builder()
1271                 if scb is _null:
1272                     scb = None
1273                     dir = self.dir.path
1274                     sccspath = os.path.join('SCCS', 's.' + self.name)
1275                     if dir != '.':
1276                         sccspath = os.path.join(dir, sccspath)
1277                     if os.path.exists(sccspath):
1278                         scb = get_DefaultSCCSBuilder()
1279                     else:
1280                         rcspath = os.path.join('RCS', self.name + ',v')
1281                         if dir != '.':
1282                             rcspath = os.path.join(dir, rcspath)
1283                         if os.path.exists(rcspath):
1284                             scb = get_DefaultRCSBuilder()
1285                 self.builder = scb
1286             self.sbuilder = scb
1287         return not scb is None
1288
1289     def is_derived(self):
1290         """Return whether this file is a derived file or not.
1291
1292         This overrides the base class method to account for the fact
1293         that a file may be derived transparently from a source code
1294         builder.
1295         """
1296         return self.has_builder() or self.side_effect or self.has_src_builder()
1297
1298     def alter_targets(self):
1299         """Return any corresponding targets in a build directory.
1300         """
1301         if self.has_builder():
1302             return [], None
1303         return self.fs.build_dir_target_climb(self.dir, [self.name])
1304
1305     def prepare(self):
1306         """Prepare for this file to be created."""
1307
1308         SCons.Node.Node.prepare(self)
1309
1310         if self.get_state() != SCons.Node.up_to_date:
1311             if self.exists():
1312                 if self.has_builder() and not self.precious:
1313                     try:
1314                         Unlink(self, None, None)
1315                     except OSError, e:
1316                         raise SCons.Errors.BuildError(node = self,
1317                                                       errstr = e.strerror)
1318                     if hasattr(self, '_exists'):
1319                         delattr(self, '_exists')
1320             else:
1321                 try:
1322                     self._createDir()
1323                 except SCons.Errors.StopError, drive:
1324                     desc = "No drive `%s' for target `%s'." % (drive, self)
1325                     raise SCons.Errors.StopError, desc
1326
1327     def remove(self):
1328         """Remove this file."""
1329         if _existsp(self.path):
1330             os.unlink(self.path)
1331             return 1
1332         return None
1333
1334     def exists(self):
1335         # Duplicate from source path if we are set up to do this.
1336         if self.duplicate and not self.has_builder() and not self.linked:
1337             src=self.srcnode().rfile()
1338             if src.exists() and src.abspath != self.abspath:
1339                 self._createDir()
1340                 try:
1341                     Unlink(self, None, None)
1342                 except OSError:
1343                     pass
1344                 try:
1345                     Link(self, src, None)
1346                 except IOError, e:
1347                     desc = "Cannot duplicate `%s' in `%s': %s." % (src, self.dir, e.strerror)
1348                     raise SCons.Errors.StopError, desc
1349                 self.linked = 1
1350                 # The Link() action may or may not have actually
1351                 # created the file, depending on whether the -n
1352                 # option was used or not.  Delete the _exists and
1353                 # _rexists attributes so they can be reevaluated.
1354                 if hasattr(self, '_exists'):
1355                     delattr(self, '_exists')
1356                 if hasattr(self, '_rexists'):
1357                     delattr(self, '_rexists')
1358         return Entry.exists(self)
1359
1360     def current(self, calc):
1361         bsig = calc.bsig(self)
1362         if not self.exists():
1363             # The file doesn't exist locally...
1364             r = self.rfile()
1365             if r != self:
1366                 # ...but there is one in a Repository...
1367                 if calc.current(r, bsig):
1368                     # ...and it's even up-to-date...
1369                     if self._local:
1370                         # ...and they'd like a local copy.
1371                         LocalCopy(self, r, None)
1372                         self.set_bsig(bsig)
1373                         self.store_bsig()
1374                     return 1
1375             self._rfile = self
1376             return None
1377         else:
1378             return calc.current(self, bsig)
1379
1380     def rfile(self):
1381         if not hasattr(self, '_rfile'):
1382             self._rfile = self
1383             if not self.exists():
1384                 n = self.fs.Rsearch(self.path, clazz=File,
1385                                     cwd=self.fs.Top)
1386                 if n:
1387                     self._rfile = n
1388         return self._rfile
1389
1390     def rstr(self):
1391         return str(self.rfile())
1392
1393     def cachepath(self):
1394         if self.fs.CachePath:
1395             bsig = self.get_bsig()
1396             if bsig is None:
1397                 raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1398             bsig = str(bsig)
1399             subdir = string.upper(bsig[0])
1400             dir = os.path.join(self.fs.CachePath, subdir)
1401             return dir, os.path.join(dir, bsig)
1402         return None, None
1403
1404 default_fs = FS()
1405
1406
1407 def find_file(filename, paths, node_factory = default_fs.File):
1408     """
1409     find_file(str, [Dir()]) -> [nodes]
1410
1411     filename - a filename to find
1412     paths - a list of directory path *nodes* to search in
1413
1414     returns - the node created from the found file.
1415
1416     Find a node corresponding to either a derived file or a file
1417     that exists already.
1418
1419     Only the first file found is returned, and none is returned
1420     if no file is found.
1421     """
1422     retval = None
1423     for dir in paths:
1424         try:
1425             node = node_factory(filename, dir)
1426             # Return true of the node exists or is a derived node.
1427             if node.has_builder() or \
1428                (isinstance(node, SCons.Node.FS.Entry) and node.exists()):
1429                 retval = node
1430                 break
1431         except TypeError:
1432             # If we find a directory instead of a file, we don't care
1433             pass
1434
1435     return retval
1436
1437 def find_files(filenames, paths, node_factory = default_fs.File):
1438     """
1439     find_files([str], [Dir()]) -> [nodes]
1440
1441     filenames - a list of filenames to find
1442     paths - a list of directory path *nodes* to search in
1443
1444     returns - the nodes created from the found files.
1445
1446     Finds nodes corresponding to either derived files or files
1447     that exist already.
1448
1449     Only the first file found is returned for each filename,
1450     and any files that aren't found are ignored.
1451     """
1452     nodes = map(lambda x, paths=paths, node_factory=node_factory:
1453                        find_file(x, paths, node_factory),
1454                 filenames)
1455     return filter(lambda x: x != None, nodes)