Document the -f option correctly, support building a parallel tree by pointing to...
[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 from UserDict import UserDict
45
46 import SCons.Action
47 import SCons.Errors
48 import SCons.Node
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 = source[0].path
73     dest = target[0].path
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 not os.path.isdir(cachedir):
134         os.mkdir(cachedir)
135     shutil.copy2(t.path, cachefile)
136     st = os.stat(t.path)
137     os.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
138
139 CachePush = SCons.Action.Action(CachePushFunc, None)
140
141 #
142 class ParentOfRoot:
143     """
144     An instance of this class is used as the parent of the root of a
145     filesystem (POSIX) or drive (Win32). This isn't actually a node,
146     but it looks enough like one so that we don't have to have
147     special purpose code everywhere to deal with dir being None. 
148     This class is an instance of the Null object pattern.
149     """
150     def __init__(self):
151         self.abspath = ''
152         self.path = ''
153         self.abspath_ = ''
154         self.path_ = ''
155         self.name=''
156         self.duplicate=0
157         self.srcdir=None
158         
159     def is_under(self, dir):
160         return 0
161
162     def up(self):
163         return None
164
165     def getRepositories(self):
166         return []
167
168     def get_dir(self):
169         return None
170
171     def recurse_get_path(self, dir, path_elems):
172         return path_elems
173
174     def src_builder(self):
175         return None
176
177 if os.path.normcase("TeSt") == os.path.normpath("TeSt"):
178     def _my_normcase(x):
179         return x
180 else:
181     def _my_normcase(x):
182         return string.upper(x)
183
184 class Entry(SCons.Node.Node):
185     """A generic class for file system entries.  This class is for
186     when we don't know yet whether the entry being looked up is a file
187     or a directory.  Instances of this class can morph into either
188     Dir or File objects by a later, more precise lookup.
189
190     Note: this class does not define __cmp__ and __hash__ for efficiency
191     reasons.  SCons does a lot of comparing of Entry objects, and so that
192     operation must be as fast as possible, which means we want to use
193     Python's built-in object identity comparison.
194     """
195
196     def __init__(self, name, directory, fs):
197         """Initialize a generic file system Entry.
198         
199         Call the superclass initialization, take care of setting up
200         our relative and absolute paths, identify our parent
201         directory, and indicate that this node should use
202         signatures."""
203         SCons.Node.Node.__init__(self)
204
205         self.name = name
206         self.fs = fs
207         self.relpath = {}
208
209         assert directory, "A directory must be provided"
210
211         self.abspath = directory.abspath_ + name
212         if directory.path == '.':
213             self.path = name
214         else:
215             self.path = directory.path_ + name
216
217         self.path_ = self.path
218         self.abspath_ = self.abspath
219         self.dir = directory
220         self.cwd = None # will hold the SConscript directory for target nodes
221         self.duplicate = directory.duplicate
222
223     def get_dir(self):
224         return self.dir
225
226     def __str__(self):
227         """A FS node's string representation is its path name."""
228         if self.duplicate or self.has_builder():
229             return self.get_path()
230         return self.srcnode().get_path()
231
232     def get_contents(self):
233         """Fetch the contents of the entry.
234         
235         Since this should return the real contents from the file
236         system, we check to see into what sort of subclass we should
237         morph this Entry."""
238         if os.path.isfile(self.abspath):
239             self.__class__ = File
240             self._morph()
241             return File.get_contents(self)
242         if os.path.isdir(self.abspath):
243             self.__class__ = Dir
244             self._morph()
245             return Dir.get_contents(self)
246         raise AttributeError
247
248     def exists(self):
249         try:
250             return self._exists
251         except AttributeError:
252             self._exists = os.path.exists(self.abspath)
253             return self._exists
254
255     def rexists(self):
256         if not hasattr(self, '_rexists'):
257             self._rexists = self.rfile().exists()
258         return self._rexists
259
260     def get_parents(self):
261         parents = SCons.Node.Node.get_parents(self)
262         if self.dir and not isinstance(self.dir, ParentOfRoot):
263             parents.append(self.dir)
264         return parents
265
266     def current(self, calc):
267         """If the underlying path doesn't exist, we know the node is
268         not current without even checking the signature, so return 0.
269         Otherwise, return None to indicate that signature calculation
270         should proceed as normal to find out if the node is current."""
271         bsig = calc.bsig(self)
272         if not self.exists():
273             return 0
274         return calc.current(self, bsig)
275
276     def is_under(self, dir):
277         if self is dir:
278             return 1
279         else:
280             return self.dir.is_under(dir)
281
282     def set_local(self):
283         self._local = 1
284
285     def srcnode(self):
286         """If this node is in a build path, return the node
287         corresponding to its source file.  Otherwise, return
288         ourself."""
289         try:
290             return self._srcnode
291         except AttributeError:
292             dir=self.dir
293             name=self.name
294             while dir:
295                 if dir.srcdir:
296                     self._srcnode = self.fs.Entry(name, dir.srcdir,
297                                                   klass=self.__class__)
298                     return self._srcnode
299                 name = dir.name + os.sep + name
300                 dir=dir.get_dir()
301             self._srcnode = self
302             return self._srcnode
303
304     def recurse_get_path(self, dir, path_elems):
305         """Recursively build a path relative to a supplied directory
306         node."""
307         if self != dir:
308             path_elems.append(self.name)
309             path_elems = self.dir.recurse_get_path(dir, path_elems)
310         return path_elems
311
312     def get_path(self, dir=None):
313         """Return path relative to the current working directory of the
314         FS object that owns us."""
315         if not dir:
316             dir = self.fs.getcwd()
317         try:
318             return self.relpath[dir]
319         except KeyError:
320             if self == dir:
321                 # Special case, return "." as the path
322                 ret = '.'
323             else:
324                 path_elems = self.recurse_get_path(dir, [])
325                 path_elems.reverse()
326                 ret = string.join(path_elems, os.sep)
327             self.relpath[dir] = ret
328             return ret
329             
330     def set_src_builder(self, builder):
331         """Set the source code builder for this node."""
332         self.sbuilder = builder
333
334     def src_builder(self):
335         """Fetch the source code builder for this node.
336
337         If there isn't one, we cache the source code builder specified
338         for the directory (which in turn will cache the value from its
339         parent directory, and so on up to the file system root).
340         """
341         try:
342             scb = self.sbuilder
343         except AttributeError:
344             scb = self.dir.src_builder()
345             self.sbuilder = scb
346         return scb
347
348 # This is for later so we can differentiate between Entry the class and Entry
349 # the method of the FS class.
350 _classEntry = Entry
351
352
353 class FS:
354     def __init__(self, path = None):
355         """Initialize the Node.FS subsystem.
356
357         The supplied path is the top of the source tree, where we
358         expect to find the top-level build file.  If no path is
359         supplied, the current directory is the default.
360
361         The path argument must be a valid absolute path.
362         """
363         if path == None:
364             self.pathTop = os.getcwd()
365         else:
366             self.pathTop = path
367         self.Root = {}
368         self.Top = None
369         self.SConstruct = None
370         self.CachePath = None
371         self.cache_force = None
372         self.cache_show = None
373
374     def set_toplevel_dir(self, path):
375         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."
376         self.pathTop = path
377
378     def set_SConstruct(self, path):
379         self.SConstruct = self.File(path)
380         
381     def __setTopLevelDir(self):
382         if not self.Top:
383             self.Top = self.__doLookup(Dir, os.path.normpath(self.pathTop))
384             self.Top.path = '.'
385             self.Top.path_ = '.' + os.sep
386             self._cwd = self.Top
387         
388     def getcwd(self):
389         self.__setTopLevelDir()
390         return self._cwd
391
392     def __checkClass(self, node, klass):
393         if klass == Entry:
394             return node
395         if node.__class__ == Entry:
396             node.__class__ = klass
397             node._morph()
398             return node
399         if not isinstance(node, klass):
400             raise TypeError, "Tried to lookup %s '%s' as a %s." % \
401                   (node.__class__.__name__, node.path, klass.__name__)
402         return node
403         
404     def __doLookup(self, fsclass, name, directory = None, create = 1):
405         """This method differs from the File and Dir factory methods in
406         one important way: the meaning of the directory parameter.
407         In this method, if directory is None or not supplied, the supplied
408         name is expected to be an absolute path.  If you try to look up a
409         relative path with directory=None, then an AssertionError will be
410         raised."""
411
412         if not name:
413             # This is a stupid hack to compensate for the fact
414             # that the POSIX and Win32 versions of os.path.normpath()
415             # behave differently.  In particular, in POSIX:
416             #   os.path.normpath('./') == '.'
417             # in Win32
418             #   os.path.normpath('./') == ''
419             #   os.path.normpath('.\\') == ''
420             #
421             # This is a definite bug in the Python library, but we have
422             # to live with it.
423             name = '.'
424         path_comp = string.split(name, os.sep)
425         drive, path_first = os.path.splitdrive(path_comp[0])
426         if not path_first:
427             # Absolute path
428             drive = _my_normcase(drive)
429             try:
430                 directory = self.Root[drive]
431             except KeyError:
432                 if not create:
433                     raise SCons.Errors.UserError
434                 dir = Dir(drive, ParentOfRoot(), self)
435                 dir.path = dir.path + os.sep
436                 dir.abspath = dir.abspath + os.sep
437                 self.Root[drive] = dir
438                 directory = dir
439             path_comp = path_comp[1:]
440         else:
441             path_comp = [ path_first, ] + path_comp[1:]
442             
443         # Lookup the directory
444         for path_name in path_comp[:-1]:
445             path_norm = _my_normcase(path_name)
446             try:
447                 directory = self.__checkClass(directory.entries[path_norm],
448                                               Dir)
449             except KeyError:
450                 if not create:
451                     raise SCons.Errors.UserError
452
453                 # look at the actual filesystem and make sure there isn't
454                 # a file already there
455                 path = directory.path_ + path_name
456                 if os.path.isfile(path):
457                     raise TypeError, \
458                           "File %s found where directory expected." % path
459
460                 dir_temp = Dir(path_name, directory, self)
461                 directory.entries[path_norm] = dir_temp
462                 directory.add_wkid(dir_temp)
463                 directory = dir_temp
464         file_name = _my_normcase(path_comp[-1])
465         try:
466             ret = self.__checkClass(directory.entries[file_name], fsclass)
467         except KeyError:
468             if not create:
469                 raise SCons.Errors.UserError
470
471             # make sure we don't create File nodes when there is actually
472             # a directory at that path on the disk, and vice versa
473             path = directory.path_ + path_comp[-1]
474             if fsclass == File:
475                 if os.path.isdir(path):
476                     raise TypeError, \
477                           "Directory %s found where file expected." % path
478             elif fsclass == Dir:
479                 if os.path.isfile(path):
480                     raise TypeError, \
481                           "File %s found where directory expected." % path
482             
483             ret = fsclass(path_comp[-1], directory, self)
484             directory.entries[file_name] = ret
485             directory.add_wkid(ret)
486         return ret
487
488     def __transformPath(self, name, directory):
489         """Take care of setting up the correct top-level directory,
490         usually in preparation for a call to doLookup().
491
492         If the path name is prepended with a '#', then it is unconditionally
493         interpreted as relative to the top-level directory of this FS.
494
495         If directory is None, and name is a relative path,
496         then the same applies.
497         """
498         self.__setTopLevelDir()
499         if name and name[0] == '#':
500             directory = self.Top
501             name = name[1:]
502             if name and (name[0] == os.sep or name[0] == '/'):
503                 # Correct such that '#/foo' is equivalent
504                 # to '#foo'.
505                 name = name[1:]
506             name = os.path.join('.', os.path.normpath(name))
507         elif not directory:
508             directory = self._cwd
509         return (os.path.normpath(name), directory)
510
511     def chdir(self, dir, change_os_dir=0):
512         """Change the current working directory for lookups.
513         If change_os_dir is true, we will also change the "real" cwd
514         to match.
515         """
516         self.__setTopLevelDir()
517         if not dir is None:
518             self._cwd = dir
519             if change_os_dir:
520                 os.chdir(dir.abspath)
521
522     def Entry(self, name, directory = None, create = 1, klass=None):
523         """Lookup or create a generic Entry node with the specified name.
524         If the name is a relative path (begins with ./, ../, or a file
525         name), then it is looked up relative to the supplied directory
526         node, or to the top level directory of the FS (supplied at
527         construction time) if no directory is supplied.
528         """
529
530         if not klass:
531             klass = Entry
532
533         if isinstance(name, Entry):
534             return self.__checkClass(name, klass)
535         else:
536             if directory and not isinstance(directory, Dir):
537                 directory = self.Dir(directory)
538             name, directory = self.__transformPath(name, directory)
539             return self.__doLookup(klass, name, directory, create)
540     
541     def File(self, name, directory = None, create = 1):
542         """Lookup or create a File node with the specified name.  If
543         the name is a relative path (begins with ./, ../, or a file name),
544         then it is looked up relative to the supplied directory node,
545         or to the top level directory of the FS (supplied at construction
546         time) if no directory is supplied.
547
548         This method will raise TypeError if a directory is found at the
549         specified path.
550         """
551
552         return self.Entry(name, directory, create, File)
553     
554     def Dir(self, name, directory = None, create = 1):
555         """Lookup or create a Dir node with the specified name.  If
556         the name is a relative path (begins with ./, ../, or a file name),
557         then it is looked up relative to the supplied directory node,
558         or to the top level directory of the FS (supplied at construction
559         time) if no directory is supplied.
560
561         This method will raise TypeError if a normal file is found at the
562         specified path.
563         """
564
565         return self.Entry(name, directory, create, Dir)
566     
567     def BuildDir(self, build_dir, src_dir, duplicate=1):
568         """Link the supplied build directory to the source directory
569         for purposes of building files."""
570         
571         self.__setTopLevelDir()
572         if not isinstance(src_dir, SCons.Node.Node):
573             src_dir = self.Dir(src_dir)
574         if not isinstance(build_dir, SCons.Node.Node):
575             build_dir = self.Dir(build_dir)
576         if not src_dir.is_under(self.Top):
577             raise SCons.Errors.UserError, "Source directory must be under top of build tree."
578         if src_dir.is_under(build_dir):
579             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
580         build_dir.link(src_dir, duplicate)
581
582     def Repository(self, *dirs):
583         """Specify Repository directories to search."""
584         for d in dirs:
585             if not isinstance(d, SCons.Node.Node):
586                 d = self.Dir(d)
587             self.__setTopLevelDir()
588             self.Top.addRepository(d)
589
590     def Rsearch(self, path, clazz=_classEntry, cwd=None):
591         """Search for something in a Repository.  Returns the first
592         one found in the list, or None if there isn't one."""
593         if isinstance(path, SCons.Node.Node):
594             return path
595         else:
596             name, d = self.__transformPath(path, cwd)
597             n = self.__doLookup(clazz, name, d)
598             if n.exists():
599                 return n
600             d = n.get_dir()
601             name = n.name
602             # Search repositories of all directories that this file is under.
603             while d:
604                 for rep in d.getRepositories():
605                     try:
606                         rnode = self.__doLookup(clazz, name, rep)
607                         # Only find the node if it exists and it is not
608                         # a derived file.  If for some reason, we are
609                         # explicitly building a file IN a Repository, we
610                         # don't want it to show up in the build tree.
611                         # This is usually the case with BuildDir().
612                         # We only want to find pre-existing files.
613                         if rnode.exists() and \
614                            (isinstance(rnode, Dir) or not rnode.has_builder()):
615                             return rnode
616                     except TypeError:
617                         pass # Wrong type of node.
618                 # Prepend directory name
619                 name = d.name + os.sep + name
620                 # Go up one directory
621                 d = d.get_dir()
622         return None
623             
624
625     def Rsearchall(self, pathlist, must_exist=1, clazz=_classEntry, cwd=None):
626         """Search for a list of somethings in the Repository list."""
627         ret = []
628         if SCons.Util.is_String(pathlist):
629             pathlist = string.split(pathlist, os.pathsep)
630         if not SCons.Util.is_List(pathlist):
631             pathlist = [pathlist]
632         for path in pathlist:
633             if isinstance(path, SCons.Node.Node):
634                 ret.append(path)
635             else:
636                 name, d = self.__transformPath(path, cwd)
637                 n = self.__doLookup(clazz, name, d)
638                 if not must_exist or n.exists():
639                     ret.append(n)
640                 if isinstance(n, Dir):
641                     # If this node is a directory, then any repositories
642                     # attached to this node can be repository paths.
643                     ret.extend(filter(lambda x, me=must_exist, clazz=clazz: isinstance(x, clazz) and (not me or x.exists()),
644                                       n.getRepositories()))
645                     
646                 d = n.get_dir()
647                 name = n.name
648                 # Search repositories of all directories that this file is under.
649                 while d:
650                     for rep in d.getRepositories():
651                         try:
652                             rnode = self.__doLookup(clazz, name, rep)
653                             # Only find the node if it exists (or must_exist is zero)
654                             # and it is not a derived file.  If for some reason, we
655                             # are explicitly building a file IN a Repository, we don't
656                             # want it to show up in the build tree.  This is usually the
657                             # case with BuildDir().    We only want to find pre-existing files.
658                             if (not must_exist or rnode.exists()) and \
659                                (not rnode.has_builder() or isinstance(rnode, Dir)):
660                                 ret.append(rnode)
661                         except TypeError:
662                             pass # Wrong type of node.
663                     # Prepend directory name
664                     name = d.name + os.sep + name
665                     # Go up one directory
666                     d = d.get_dir()
667         return ret
668
669     def CacheDir(self, path):
670         self.CachePath = path
671
672 # XXX TODO?
673 # Annotate with the creator
674 # rel_path
675 # linked_targets
676 # is_accessible
677
678 class Dir(Entry):
679     """A class for directories in a file system.
680     """
681
682     def __init__(self, name, directory, fs):
683         Entry.__init__(self, name, directory, fs)
684         self._morph()
685
686     def _morph(self):
687         """Turn a file system node (either a freshly initialized
688         directory object or a separate Entry object) into a
689         proper directory object.
690         
691         Modify our paths to add the trailing slash that indicates
692         a directory.  Set up this directory's entries and hook it
693         into the file system tree.  Specify that directories (this
694         node) don't use signatures for currency calculation."""
695
696         self.path_ = self.path + os.sep
697         self.abspath_ = self.abspath + os.sep
698         self.repositories = []
699         self.srcdir = None
700         
701         self.entries = {}
702         self.entries['.'] = self
703         self.entries['..'] = self.dir
704         self.cwd = self
705         self.builder = 1
706         self._sconsign = None
707         
708     def __clearRepositoryCache(self, duplicate=None):
709         """Called when we change the repository(ies) for a directory.
710         This clears any cached information that is invalidated by changing
711         the repository."""
712
713         for node in self.entries.values():
714             if node != self.dir:
715                 if node != self and isinstance(node, Dir):
716                     node.__clearRepositoryCache(duplicate)
717                 else:
718                     try:
719                         del node._srcreps
720                     except AttributeError:
721                         pass
722                     try:
723                         del node._rfile
724                     except AttributeError:
725                         pass
726                     try:
727                         del node._rexists
728                     except AttributeError:
729                         pass
730                     try:
731                         del node._exists
732                     except AttributeError:
733                         pass
734                     try:
735                         del node._srcnode
736                     except AttributeError:
737                         pass
738                     if duplicate != None:
739                         node.duplicate=duplicate
740     
741     def __resetDuplicate(self, node):
742         if node != self:
743             node.duplicate = node.get_dir().duplicate
744         
745     def Dir(self, name):
746         """Create a directory node named 'name' relative to this directory."""
747         return self.fs.Dir(name, self)
748
749     def File(self, name):
750         """Create  file node named 'name' relatove to this directory."""
751         return self.fs.File(name, self)
752                 
753     def link(self, srcdir, duplicate):
754         """Set this directory as the build directory for the
755         supplied source directory."""
756         self.srcdir = srcdir
757         self.duplicate = duplicate
758         self.__clearRepositoryCache(duplicate)
759
760     def getRepositories(self):
761         """Returns a list of repositories for this directory."""
762         if self.srcdir and not self.duplicate:
763             try:
764                 return self._srcreps
765             except AttributeError:
766                 self._srcreps = self.fs.Rsearchall(self.srcdir.path,
767                                                    clazz=Dir,
768                                                    must_exist=0,
769                                                    cwd=self.fs.Top) \
770                                 + self.repositories
771                 return self._srcreps
772         return self.repositories
773
774     def addRepository(self, dir):
775         if not dir in self.repositories and dir != self:
776             self.repositories.append(dir)
777             self.__clearRepositoryCache()
778
779     def up(self):
780         return self.entries['..']
781
782     def root(self):
783         if not self.entries['..']:
784             return self
785         else:
786             return self.entries['..'].root()
787
788     def all_children(self, scan):
789         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
790         kids = map(lambda x, s=self: s.entries[x], keys)
791         def c(one, two):
792             if one.abspath < two.abspath:
793                return -1
794             if one.abspath > two.abspath:
795                return 1
796             return 0
797         kids.sort(c)
798         return kids + SCons.Node.Node.all_children(self, 0)
799
800     def get_actions(self):
801         """A null "builder" for directories."""
802         return []
803
804     def build(self):
805         """A null "builder" for directories."""
806         pass
807
808     def calc_signature(self, calc):
809         """A directory has no signature."""
810         return None
811
812     def set_bsig(self, bsig):
813         """A directory has no signature."""
814         bsig = None
815
816     def set_csig(self, csig):
817         """A directory has no signature."""
818         csig = None
819
820     def get_contents(self):
821         """Return a fixed "contents" value of a directory."""
822         return ''
823
824     def current(self, calc):
825         """If all of our children were up-to-date, then this
826         directory was up-to-date, too."""
827         state = 0
828         for kid in self.children(None):
829             s = kid.get_state()
830             if s and (not state or s > state):
831                 state = s
832         import SCons.Node
833         if state == 0 or state == SCons.Node.up_to_date:
834             return 1
835         else:
836             return 0
837
838     def sconsign(self):
839         """Return the .sconsign file info for this directory,
840         creating it first if necessary."""
841         if not self._sconsign:
842             import SCons.Sig
843             self._sconsign = SCons.Sig.SConsignFile(self)
844         return self._sconsign
845
846     def srcnode(self):
847         """Dir has a special need for srcnode()...if we
848         have a srcdir attribute set, then that *is* our srcnode."""
849         if self.srcdir:
850             return self.srcdir
851         return Entry.srcnode(self)
852
853 # XXX TODO?
854 # base_suf
855 # suffix
856 # addsuffix
857 # accessible
858 # relpath
859
860 class File(Entry):
861     """A class for files in a file system.
862     """
863     def __init__(self, name, directory, fs):
864         Entry.__init__(self, name, directory, fs)
865         self._morph()
866
867
868     def Dir(self, name):
869         """Create a directory node named 'name' relative to
870         the SConscript directory of this file."""
871         return self.fs.Dir(name, self.cwd)
872
873     def File(self, name):
874         """Create a file node named 'name' relative to
875         the SConscript directory of this file."""
876         return self.fs.File(name, self.cwd)
877
878     def RDirs(self, pathlist):
879         """Search for a list of directories in the Repository list."""
880         return self.fs.Rsearchall(pathlist, clazz=Dir, must_exist=0,
881                                   cwd=self.cwd)
882     
883     def generate_build_env(self):
884         env = SCons.Node.Node.generate_build_env(self)
885         
886         return env.Override({'Dir' : self.Dir,
887                              'File' : self.File,
888                              'RDirs' : self.RDirs})
889         
890     def _morph(self):
891         """Turn a file system node into a File object."""
892         self.linked = 0
893         self.scanner_paths = {}
894         self.found_includes = {}
895         if not hasattr(self, '_local'):
896             self._local = 0
897
898     def root(self):
899         return self.dir.root()
900
901     def get_contents(self):
902         if not self.rexists():
903             return ''
904         return open(self.rfile().abspath, "rb").read()
905
906     def get_timestamp(self):
907         if self.rexists():
908             return os.path.getmtime(self.rfile().abspath)
909         else:
910             return 0
911
912     def calc_signature(self, calc, cache=None):
913         """
914         Select and calculate the appropriate build signature for a File.
915
916         self - the File node
917         calc - the signature calculation module
918         cache - alternate node to use for the signature cache
919         returns - the signature
920         """
921
922         if self.has_builder():
923             if SCons.Sig.build_signature:
924                 return calc.bsig(self.rfile(), self)
925             else:
926                 return calc.csig(self.rfile(), self)
927         elif not self.rexists():
928             return None
929         else:
930             return calc.csig(self.rfile(), self)
931         
932     def store_csig(self):
933         self.dir.sconsign().set_csig(self.name, self.get_csig())
934
935     def store_bsig(self):
936         self.dir.sconsign().set_bsig(self.name, self.get_bsig())
937
938     def store_implicit(self):
939         self.dir.sconsign().set_implicit(self.name, self.implicit)
940
941     def store_timestamp(self):
942         self.dir.sconsign().set_timestamp(self.name, self.get_timestamp())
943
944     def get_prevsiginfo(self):
945         """Fetch the previous signature information from the
946         .sconsign entry."""
947         return self.dir.sconsign().get(self.name)
948
949     def get_stored_implicit(self):
950         return self.dir.sconsign().get_implicit(self.name)
951
952     def get_found_includes(self, env, scanner, target):
953         """Return the included implicit dependencies in this file.
954         Cache results so we only scan the file once regardless of
955         how many times this information is requested."""
956         if not scanner:
957             return []
958
959         try:
960             path = target.scanner_paths[scanner]
961         except AttributeError:
962             # The target had no scanner_paths attribute, which means
963             # it's an Alias or some other node that's not actually a
964             # file.  In that case, back off and use the path for this
965             # node itself.
966             try:
967                 path = self.scanner_paths[scanner]
968             except KeyError:
969                 path = scanner.path(env, self.cwd)
970                 self.scanner_paths[scanner] = path
971         except KeyError:
972             path = scanner.path(env, target.cwd)
973             target.scanner_paths[scanner] = path
974
975         try:
976             includes = self.found_includes[path]
977         except KeyError:
978             includes = scanner(self, env, path)
979             self.found_includes[path] = includes
980
981         return includes
982
983     def scanner_key(self):
984         return os.path.splitext(self.name)[1]
985
986     def _createDir(self):
987         # ensure that the directories for this node are
988         # created.
989
990         listDirs = []
991         parent=self.dir
992         while parent:
993             if parent.exists():
994                 break
995             listDirs.append(parent)
996             p = parent.up()
997             if isinstance(p, ParentOfRoot):
998                 raise SCons.Errors.StopError, parent.path
999             parent = p
1000         listDirs.reverse()
1001         for dirnode in listDirs:
1002             try:
1003                 Mkdir(dirnode, None, None)
1004                 # The Mkdir() action may or may not have actually
1005                 # created the directory, depending on whether the -n
1006                 # option was used or not.  Delete the _exists and
1007                 # _rexists attributes so they can be reevaluated.
1008                 if hasattr(dirnode, '_exists'):
1009                     delattr(dirnode, '_exists')
1010                 if hasattr(dirnode, '_rexists'):
1011                     delattr(dirnode, '_rexists')
1012             except OSError:
1013                 pass
1014
1015     def build(self):
1016         """Actually build the file.
1017
1018         This overrides the base class build() method to check for the
1019         existence of derived files in a CacheDir before going ahead and
1020         building them.
1021
1022         This method is called from multiple threads in a parallel build,
1023         so only do thread safe stuff here. Do thread unsafe stuff in
1024         built().
1025         """
1026         if not self.has_builder():
1027             return
1028         if self.fs.CachePath:
1029             if self.fs.cache_show:
1030                 if CacheRetrieveSilent(self, None, None) == 0:
1031                     def do_print(action, targets, sources, env, self=self):
1032                         al = action.strfunction(targets, self.sources, env)
1033                         if not SCons.Util.is_List(al):
1034                             al = [al]
1035                         for a in al:
1036                             action.show(a)
1037                     self._for_each_action(do_print)
1038                     return
1039             elif CacheRetrieve(self, None, None) == 0:
1040                 return
1041         SCons.Node.Node.build(self)
1042
1043     def built(self):
1044         """Called just after this node is sucessfully built."""
1045         # Push this file out to cache before the superclass Node.built()
1046         # method has a chance to clear the build signature, which it
1047         # will do if this file has a source scanner.
1048         if self.fs.CachePath and os.path.exists(self.path):
1049             CachePush(self, None, None)
1050         SCons.Node.Node.built(self)
1051         self.found_includes = {}
1052         if hasattr(self, '_exists'):
1053             delattr(self, '_exists')
1054         if hasattr(self, '_rexists'):
1055             delattr(self, '_rexists')
1056
1057     def visited(self):
1058         if self.fs.CachePath and self.fs.cache_force and os.path.exists(self.path):
1059             CachePush(self, None, None)
1060
1061     def has_builder(self, fetch = 1):
1062         """Return whether this Node has a builder or not.
1063
1064         If this Node doesn't have an explicit builder, this is where we
1065         figure out, on the fly, if there's a source code builder for it.
1066         """
1067         try:
1068             b = self.builder
1069         except AttributeError:
1070             if fetch and not os.path.exists(self.path):
1071                 b = self.src_builder()
1072             else:
1073                 b = None
1074             self.builder = b
1075         return not b is None
1076
1077     def prepare(self):
1078         """Prepare for this file to be created."""
1079
1080         def missing(node):
1081             return not node.has_builder() and not node.linked and not node.rexists()
1082         missing_sources = filter(missing, self.children())
1083         if missing_sources:
1084             desc = "No Builder for target `%s', needed by `%s'." % (missing_sources[0], self)
1085             raise SCons.Errors.StopError, desc
1086
1087         if self.exists():
1088             if self.has_builder() and not self.precious:
1089                 try:
1090                     Unlink(self, None, None)
1091                 except OSError, e:
1092                     raise SCons.Errors.BuildError(node = self,
1093                                                   errstr = e.strerror)
1094                 if hasattr(self, '_exists'):
1095                     delattr(self, '_exists')
1096         else:
1097             try:
1098                 self._createDir()
1099             except SCons.Errors.StopError, drive:
1100                 desc = "No drive `%s' for target `%s'." % (drive, self)
1101                 raise SCons.Errors.StopError, desc
1102
1103     def remove(self):
1104         """Remove this file."""
1105         if _existsp(self.path):
1106             os.unlink(self.path)
1107             return 1
1108         return None
1109
1110     def exists(self):
1111         # Duplicate from source path if we are set up to do this.
1112         if self.duplicate and not self.has_builder() and not self.linked:
1113             src=self.srcnode().rfile()
1114             if src.exists() and src.abspath != self.abspath:
1115                 self._createDir()
1116                 try:
1117                     Unlink(self, None, None)
1118                 except OSError:
1119                     pass
1120                 try:
1121                     Link(self, src, None)
1122                 except IOError, e:
1123                     desc = "Cannot duplicate `%s' in `%s': %s." % (src, self.dir, e.strerror)
1124                     raise SCons.Errors.StopError, desc
1125                 self.linked = 1
1126                 # The Link() action may or may not have actually
1127                 # created the file, depending on whether the -n
1128                 # option was used or not.  Delete the _exists and
1129                 # _rexists attributes so they can be reevaluated.
1130                 if hasattr(self, '_exists'):
1131                     delattr(self, '_exists')
1132                 if hasattr(self, '_rexists'):
1133                     delattr(self, '_rexists')
1134         return Entry.exists(self)
1135
1136     def current(self, calc):
1137         bsig = calc.bsig(self)
1138         if not self.exists():
1139             # The file doesn't exist locally...
1140             r = self.rfile()
1141             if r != self:
1142                 # ...but there is one in a Repository...
1143                 if calc.current(r, bsig):
1144                     # ...and it's even up-to-date...
1145                     if self._local:
1146                         # ...and they'd like a local copy.
1147                         LocalCopy(self, r, None)
1148                         self.set_bsig(bsig)
1149                         self.store_bsig()
1150                     return 1
1151             self._rfile = self
1152             return None
1153         else:
1154             return calc.current(self, bsig)
1155
1156     def rfile(self):
1157         if not hasattr(self, '_rfile'):
1158             self._rfile = self
1159             if not self.exists():
1160                 n = self.fs.Rsearch(self.path, clazz=File,
1161                                     cwd=self.fs.Top)
1162                 if n:
1163                     self._rfile = n
1164         return self._rfile
1165
1166     def rstr(self):
1167         return str(self.rfile())
1168
1169     def cachepath(self):
1170         if self.fs.CachePath:
1171             bsig = self.get_bsig()
1172             if bsig is None:
1173                 raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1174             bsig = str(bsig)
1175             subdir = string.upper(bsig[0])
1176             dir = os.path.join(self.fs.CachePath, subdir)
1177             return dir, os.path.join(dir, bsig)
1178         return None, None
1179
1180 default_fs = FS()
1181
1182
1183 def find_file(filename, paths, node_factory = default_fs.File):
1184     """
1185     find_file(str, [Dir()]) -> [nodes]
1186
1187     filename - a filename to find
1188     paths - a list of directory path *nodes* to search in
1189
1190     returns - the node created from the found file.
1191
1192     Find a node corresponding to either a derived file or a file
1193     that exists already.
1194
1195     Only the first file found is returned, and none is returned
1196     if no file is found.
1197     """
1198     retval = None
1199     for dir in paths:
1200         try:
1201             node = node_factory(filename, dir)
1202             # Return true of the node exists or is a derived node.
1203             if node.has_builder() or \
1204                (isinstance(node, SCons.Node.FS.Entry) and node.exists()):
1205                 retval = node
1206                 break
1207         except TypeError:
1208             # If we find a directory instead of a file, we don't care
1209             pass
1210
1211     return retval
1212
1213 def find_files(filenames, paths, node_factory = default_fs.File):
1214     """
1215     find_files([str], [Dir()]) -> [nodes]
1216
1217     filenames - a list of filenames to find
1218     paths - a list of directory path *nodes* to search in
1219
1220     returns - the nodes created from the found files.
1221
1222     Finds nodes corresponding to either derived files or files
1223     that exist already.
1224
1225     Only the first file found is returned for each filename,
1226     and any files that aren't found are ignored.
1227     """
1228     nodes = map(lambda x, paths=paths, node_factory=node_factory:
1229                        find_file(x, paths, node_factory),
1230                 filenames)
1231     return filter(lambda x: x != None, nodes)