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