9ecac89a519b94d3d30638d74ba57315daa754f7
[scons.git] / src / engine / SCons / Node / FS.py
1 """scons.Node.FS
2
3 File system nodes.
4
5 These Nodes represent the canonical external objects that people think
6 of when they think of building software: files and directories.
7
8 This holds a "default_fs" variable that should be initialized with an FS
9 that can be used by scripts or modules looking for the canonical default.
10
11 """
12
13 #
14 # __COPYRIGHT__
15 #
16 # Permission is hereby granted, free of charge, to any person obtaining
17 # a copy of this software and associated documentation files (the
18 # "Software"), to deal in the Software without restriction, including
19 # without limitation the rights to use, copy, modify, merge, publish,
20 # distribute, sublicense, and/or sell copies of the Software, and to
21 # permit persons to whom the Software is furnished to do so, subject to
22 # the following conditions:
23 #
24 # The above copyright notice and this permission notice shall be included
25 # in all copies or substantial portions of the Software.
26 #
27 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
28 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
29 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 #
35
36 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37
38 import os
39 import os.path
40 import shutil
41 import stat
42 import string
43 import sys
44 import time
45 import cStringIO
46
47 import SCons.Action
48 from SCons.Debug import logInstanceCreation
49 import SCons.Errors
50 import SCons.Node
51 import SCons.Sig.MD5
52 import SCons.Subst
53 import SCons.Util
54 import SCons.Warnings
55
56 # The max_drift value:  by default, use a cached signature value for
57 # any file that's been untouched for more than two days.
58 default_max_drift = 2*24*60*60
59
60 #
61 # We stringify these file system Nodes a lot.  Turning a file system Node
62 # into a string is non-trivial, because the final string representation
63 # can depend on a lot of factors:  whether it's a derived target or not,
64 # whether it's linked to a repository or source directory, and whether
65 # there's duplication going on.  The normal technique for optimizing
66 # calculations like this is to memoize (cache) the string value, so you
67 # only have to do the calculation once.
68 #
69 # A number of the above factors, however, can be set after we've already
70 # been asked to return a string for a Node, because a Repository() or
71 # BuildDir() call or the like may not occur until later in SConscript
72 # files.  So this variable controls whether we bother trying to save
73 # string values for Nodes.  The wrapper interface can set this whenever
74 # they're done mucking with Repository and BuildDir and the other stuff,
75 # to let this module know it can start returning saved string values
76 # for Nodes.
77 #
78 Save_Strings = None
79
80 def save_strings(val):
81     global Save_Strings
82     Save_Strings = val
83
84 #
85 # SCons.Action objects for interacting with the outside world.
86 #
87 # The Node.FS methods in this module should use these actions to
88 # create and/or remove files and directories; they should *not* use
89 # os.{link,symlink,unlink,mkdir}(), etc., directly.
90 #
91 # Using these SCons.Action objects ensures that descriptions of these
92 # external activities are properly displayed, that the displays are
93 # suppressed when the -s (silent) option is used, and (most importantly)
94 # the actions are disabled when the the -n option is used, in which case
95 # there should be *no* changes to the external file system(s)...
96 #
97
98 def _copy_func(src, dest):
99     shutil.copy2(src, dest)
100     st=os.stat(src)
101     os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
102
103 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
104                     'hard-copy', 'soft-copy', 'copy']
105
106 Link_Funcs = [] # contains the callables of the specified duplication style
107
108 def set_duplicate(duplicate):
109     # Fill in the Link_Funcs list according to the argument
110     # (discarding those not available on the platform).
111
112     # Set up the dictionary that maps the argument names to the
113     # underlying implementations.  We do this inside this function,
114     # not in the top-level module code, so that we can remap os.link
115     # and os.symlink for testing purposes.
116     try:
117         _hardlink_func = os.link
118     except AttributeError:
119         _hardlink_func = None
120
121     try:
122         _softlink_func = os.symlink
123     except AttributeError:
124         _softlink_func = None
125
126     link_dict = {
127         'hard' : _hardlink_func,
128         'soft' : _softlink_func,
129         'copy' : _copy_func
130     }
131
132     if not duplicate in Valid_Duplicates:
133         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
134                                            "should be in Valid_Duplicates")
135     global Link_Funcs
136     Link_Funcs = []
137     for func in string.split(duplicate,'-'):
138         if link_dict[func]:
139             Link_Funcs.append(link_dict[func])
140
141 def LinkFunc(target, source, env):
142     # Relative paths cause problems with symbolic links, so
143     # we use absolute paths, which may be a problem for people
144     # who want to move their soft-linked src-trees around. Those
145     # people should use the 'hard-copy' mode, softlinks cannot be
146     # used for that; at least I have no idea how ...
147     src = source[0].abspath
148     dest = target[0].abspath
149     dir, file = os.path.split(dest)
150     if dir and not target[0].fs.isdir(dir):
151         os.makedirs(dir)
152     if not Link_Funcs:
153         # Set a default order of link functions.
154         set_duplicate('hard-soft-copy')
155     # Now link the files with the previously specified order.
156     for func in Link_Funcs:
157         try:
158             func(src,dest)
159             break
160         except (IOError, OSError):
161             # An OSError indicates something happened like a permissions
162             # problem or an attempt to symlink across file-system
163             # boundaries.  An IOError indicates something like the file
164             # not existing.  In either case, keeping trying additional
165             # functions in the list and only raise an error if the last
166             # one failed.
167             if func == Link_Funcs[-1]:
168                 # exception of the last link method (copy) are fatal
169                 raise
170             else:
171                 pass
172     return 0
173
174 Link = SCons.Action.Action(LinkFunc, None)
175 def LocalString(target, source, env):
176     return 'Local copy of %s from %s' % (target[0], source[0])
177
178 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
179
180 def UnlinkFunc(target, source, env):
181     t = target[0]
182     t.fs.unlink(t.abspath)
183     return 0
184
185 Unlink = SCons.Action.Action(UnlinkFunc, None)
186
187 def MkdirFunc(target, source, env):
188     t = target[0]
189     if not t.exists():
190         t.fs.mkdir(t.abspath)
191     return 0
192
193 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
194
195 MkdirBuilder = None
196
197 def get_MkdirBuilder():
198     global MkdirBuilder
199     if MkdirBuilder is None:
200         import SCons.Builder
201         # "env" will get filled in by Executor.get_build_env()
202         # calling SCons.Defaults.DefaultEnvironment() when necessary.
203         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
204                                              env = None,
205                                              explain = None,
206                                              is_explicit = None,
207                                              name = "MkdirBuilder")
208     return MkdirBuilder
209
210 def CacheRetrieveFunc(target, source, env):
211     t = target[0]
212     fs = t.fs
213     cachedir, cachefile = t.cachepath()
214     if fs.exists(cachefile):
215         if SCons.Action.execute_actions:
216             fs.copy2(cachefile, t.path)
217             st = fs.stat(cachefile)
218             fs.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
219         return 0
220     return 1
221
222 def CacheRetrieveString(target, source, env):
223     t = target[0]
224     cachedir, cachefile = t.cachepath()
225     if t.fs.exists(cachefile):
226         return "Retrieved `%s' from cache" % t.path
227     return None
228
229 CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
230
231 CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
232
233 def CachePushFunc(target, source, env):
234     t = target[0]
235     fs = t.fs
236     cachedir, cachefile = t.cachepath()
237     if fs.exists(cachefile):
238         # Don't bother copying it if it's already there.
239         return
240
241     if not fs.isdir(cachedir):
242         fs.makedirs(cachedir)
243
244     tempfile = cachefile+'.tmp'
245     try:
246         fs.copy2(t.path, tempfile)
247         fs.rename(tempfile, cachefile)
248         st = fs.stat(t.path)
249         fs.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
250     except (IOError, OSError):
251         # It's possible someone else tried writing the file at the
252         # same time we did, or else that there was some problem like
253         # the CacheDir being on a separate file system that's full.
254         # In any case, inability to push a file to cache doesn't affect
255         # the correctness of the build, so just print a warning.
256         SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning,
257                             "Unable to copy %s to cache. Cache file is %s"
258                                 % (str(target), cachefile))
259         return
260
261 CachePush = SCons.Action.Action(CachePushFunc, None)
262
263 class _Null:
264     pass
265
266 _null = _Null()
267
268 DefaultSCCSBuilder = None
269 DefaultRCSBuilder = None
270
271 def get_DefaultSCCSBuilder():
272     global DefaultSCCSBuilder
273     if DefaultSCCSBuilder is None:
274         import SCons.Builder
275         # "env" will get filled in by Executor.get_build_env()
276         # calling SCons.Defaults.DefaultEnvironment() when necessary.
277         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
278         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
279                                                    env = None,
280                                                    name = "DefaultSCCSBuilder")
281     return DefaultSCCSBuilder
282
283 def get_DefaultRCSBuilder():
284     global DefaultRCSBuilder
285     if DefaultRCSBuilder is None:
286         import SCons.Builder
287         # "env" will get filled in by Executor.get_build_env()
288         # calling SCons.Defaults.DefaultEnvironment() when necessary.
289         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
290         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
291                                                   env = None,
292                                                   name = "DefaultRCSBuilder")
293     return DefaultRCSBuilder
294
295 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
296 _is_cygwin = sys.platform == "cygwin"
297 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
298     def _my_normcase(x):
299         return x
300 else:
301     def _my_normcase(x):
302         return string.upper(x)
303
304
305
306 class DiskChecker:
307     def __init__(self, type, do, ignore):
308         self.type = type
309         self.do = do
310         self.ignore = ignore
311         self.set_do()
312     def set_do(self):
313         self.__call__ = self.do
314     def set_ignore(self):
315         self.__call__ = self.ignore
316     def set(self, list):
317         if self.type in list:
318             self.set_do()
319         else:
320             self.set_ignore()
321
322 def do_diskcheck_match(node, predicate, errorfmt):
323     path = node.abspath
324     if predicate(path):
325         raise TypeError, errorfmt % path
326
327 def ignore_diskcheck_match(node, predicate, errorfmt):
328     pass
329
330 def do_diskcheck_rcs(node, name):
331     rcspath = 'RCS' + os.sep + name+',v'
332     return node.entry_exists_on_disk(rcspath)
333
334 def ignore_diskcheck_rcs(node, name):
335     return None
336
337 def do_diskcheck_sccs(node, name):
338     sccspath = 'SCCS' + os.sep + 's.'+name
339     return node.entry_exists_on_disk(sccspath)
340
341 def ignore_diskcheck_sccs(node, name):
342     return None
343
344 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
345 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
346 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
347
348 diskcheckers = [
349     diskcheck_match,
350     diskcheck_rcs,
351     diskcheck_sccs,
352 ]
353
354 def set_diskcheck(list):
355     for dc in diskcheckers:
356         dc.set(list)
357
358 def diskcheck_types():
359     return map(lambda dc: dc.type, diskcheckers)
360
361
362
363 class EntryProxy(SCons.Util.Proxy):
364     def __get_abspath(self):
365         entry = self.get()
366         return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
367                                              entry.name + "_abspath")
368
369     def __get_filebase(self):
370         name = self.get().name
371         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
372                                              name + "_filebase")
373
374     def __get_suffix(self):
375         name = self.get().name
376         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
377                                              name + "_suffix")
378
379     def __get_file(self):
380         name = self.get().name
381         return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
382
383     def __get_base_path(self):
384         """Return the file's directory and file name, with the
385         suffix stripped."""
386         entry = self.get()
387         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
388                                              entry.name + "_base")
389
390     def __get_posix_path(self):
391         """Return the path with / as the path separator,
392         regardless of platform."""
393         if os.sep == '/':
394             return self
395         else:
396             entry = self.get()
397             r = string.replace(entry.get_path(), os.sep, '/')
398             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
399
400     def __get_win32_path(self):
401         """Return the path with \ as the path separator,
402         regardless of platform."""
403         if os.sep == '\\':
404             return self
405         else:
406             entry = self.get()
407             r = string.replace(entry.get_path(), os.sep, '\\')
408             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_win32")
409
410     def __get_srcnode(self):
411         return EntryProxy(self.get().srcnode())
412
413     def __get_srcdir(self):
414         """Returns the directory containing the source node linked to this
415         node via BuildDir(), or the directory of this node if not linked."""
416         return EntryProxy(self.get().srcnode().dir)
417
418     def __get_rsrcnode(self):
419         return EntryProxy(self.get().srcnode().rfile())
420
421     def __get_rsrcdir(self):
422         """Returns the directory containing the source node linked to this
423         node via BuildDir(), or the directory of this node if not linked."""
424         return EntryProxy(self.get().srcnode().rfile().dir)
425
426     def __get_dir(self):
427         return EntryProxy(self.get().dir)
428     
429     dictSpecialAttrs = { "base"     : __get_base_path,
430                          "posix"    : __get_posix_path,
431                          "win32"    : __get_win32_path,
432                          "srcpath"  : __get_srcnode,
433                          "srcdir"   : __get_srcdir,
434                          "dir"      : __get_dir,
435                          "abspath"  : __get_abspath,
436                          "filebase" : __get_filebase,
437                          "suffix"   : __get_suffix,
438                          "file"     : __get_file,
439                          "rsrcpath" : __get_rsrcnode,
440                          "rsrcdir"  : __get_rsrcdir,
441                        }
442
443     def __getattr__(self, name):
444         # This is how we implement the "special" attributes
445         # such as base, posix, srcdir, etc.
446         try:
447             return self.dictSpecialAttrs[name](self)
448         except KeyError:
449             try:
450                 attr = SCons.Util.Proxy.__getattr__(self, name)
451             except AttributeError:
452                 entry = self.get()
453                 classname = string.split(str(entry.__class__), '.')[-1]
454                 if classname[-2:] == "'>":
455                     # new-style classes report their name as:
456                     #   "<class 'something'>"
457                     # instead of the classic classes:
458                     #   "something"
459                     classname = classname[:-2]
460                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
461             return attr
462
463 class Base(SCons.Node.Node):
464     """A generic class for file system entries.  This class is for
465     when we don't know yet whether the entry being looked up is a file
466     or a directory.  Instances of this class can morph into either
467     Dir or File objects by a later, more precise lookup.
468
469     Note: this class does not define __cmp__ and __hash__ for
470     efficiency reasons.  SCons does a lot of comparing of
471     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
472     as fast as possible, which means we want to use Python's built-in
473     object identity comparisons.
474     """
475
476     def __init__(self, name, directory, fs):
477         """Initialize a generic Node.FS.Base object.
478         
479         Call the superclass initialization, take care of setting up
480         our relative and absolute paths, identify our parent
481         directory, and indicate that this node should use
482         signatures."""
483         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
484         SCons.Node.Node.__init__(self)
485
486         self.name = name
487         self.fs = fs
488
489         assert directory, "A directory must be provided"
490
491         self.abspath = directory.entry_abspath(name)
492         if directory.path == '.':
493             self.path = name
494         else:
495             self.path = directory.entry_path(name)
496         if directory.tpath == '.':
497             self.tpath = name
498         else:
499             self.tpath = directory.entry_tpath(name)
500         self.path_elements = directory.path_elements + [self]
501
502         self.dir = directory
503         self.cwd = None # will hold the SConscript directory for target nodes
504         self.duplicate = directory.duplicate
505
506     def clear(self):
507         """Completely clear a Node.FS.Base object of all its cached
508         state (so that it can be re-evaluated by interfaces that do
509         continuous integration builds).
510         __cache_reset__
511         """
512         SCons.Node.Node.clear(self)
513
514     def get_dir(self):
515         return self.dir
516
517     def get_suffix(self):
518         "__cacheable__"
519         return SCons.Util.splitext(self.name)[1]
520
521     def rfile(self):
522         return self
523
524     def __str__(self):
525         """A Node.FS.Base object's string representation is its path
526         name."""
527         global Save_Strings
528         if Save_Strings:
529             return self._save_str()
530         return self._get_str()
531
532     def _save_str(self):
533         "__cacheable__"
534         return self._get_str()
535
536     def _get_str(self):
537         if self.duplicate or self.is_derived():
538             return self.get_path()
539         return self.srcnode().get_path()
540
541     rstr = __str__
542
543     def stat(self):
544         "__cacheable__"
545         try: return self.fs.stat(self.abspath)
546         except os.error: return None
547
548     def exists(self):
549         "__cacheable__"
550         return not self.stat() is None
551
552     def rexists(self):
553         "__cacheable__"
554         return self.rfile().exists()
555
556     def getmtime(self):
557         st = self.stat()
558         if st:
559             return self.stat()[stat.ST_MTIME]
560         else:
561             return None
562
563     def getsize(self):
564         st = self.stat()
565         if st:
566             return self.stat()[stat.ST_SIZE]
567         else:
568             return None
569
570     def isdir(self):
571         st = self.stat()
572         return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
573
574     def isfile(self):
575         st = self.stat()
576         return not st is None and stat.S_ISREG(st[stat.ST_MODE])
577
578     if hasattr(os, 'symlink'):
579         def islink(self):
580             try: st = self.fs.lstat(self.abspath)
581             except os.error: return 0
582             return stat.S_ISLNK(st[stat.ST_MODE])
583     else:
584         def islink(self):
585             return 0                    # no symlinks
586
587     def is_under(self, dir):
588         if self is dir:
589             return 1
590         else:
591             return self.dir.is_under(dir)
592
593     def set_local(self):
594         self._local = 1
595
596     def srcnode(self):
597         """If this node is in a build path, return the node
598         corresponding to its source file.  Otherwise, return
599         ourself.
600         __cacheable__"""
601         dir=self.dir
602         name=self.name
603         while dir:
604             if dir.srcdir:
605                 srcnode = self.fs.Entry(name, dir.srcdir,
606                                         klass=self.__class__)
607                 return srcnode
608             name = dir.name + os.sep + name
609             dir = dir.up()
610         return self
611
612     def get_path(self, dir=None):
613         """Return path relative to the current working directory of the
614         Node.FS.Base object that owns us."""
615         if not dir:
616             dir = self.fs.getcwd()
617         if self == dir:
618             return '.'
619         path_elems = self.path_elements
620         try: i = path_elems.index(dir)
621         except ValueError: pass
622         else: path_elems = path_elems[i+1:]
623         path_elems = map(lambda n: n.name, path_elems)
624         return string.join(path_elems, os.sep)
625
626     def set_src_builder(self, builder):
627         """Set the source code builder for this node."""
628         self.sbuilder = builder
629         if not self.has_builder():
630             self.builder_set(builder)
631
632     def src_builder(self):
633         """Fetch the source code builder for this node.
634
635         If there isn't one, we cache the source code builder specified
636         for the directory (which in turn will cache the value from its
637         parent directory, and so on up to the file system root).
638         """
639         try:
640             scb = self.sbuilder
641         except AttributeError:
642             scb = self.dir.src_builder()
643             self.sbuilder = scb
644         return scb
645
646     def get_abspath(self):
647         """Get the absolute path of the file."""
648         return self.abspath
649
650     def for_signature(self):
651         # Return just our name.  Even an absolute path would not work,
652         # because that can change thanks to symlinks or remapped network
653         # paths.
654         return self.name
655
656     def get_subst_proxy(self):
657         try:
658             return self._proxy
659         except AttributeError:
660             ret = EntryProxy(self)
661             self._proxy = ret
662             return ret
663
664 class Entry(Base):
665     """This is the class for generic Node.FS entries--that is, things
666     that could be a File or a Dir, but we're just not sure yet.
667     Consequently, the methods in this class really exist just to
668     transform their associated object into the right class when the
669     time comes, and then call the same-named method in the transformed
670     class."""
671
672     def diskcheck_match(self):
673         pass
674
675     def disambiguate(self):
676         if self.isdir():
677             self.__class__ = Dir
678             self._morph()
679         else:
680             self.__class__ = File
681             self._morph()
682             self.clear()
683         return self
684
685     def rfile(self):
686         """We're a generic Entry, but the caller is actually looking for
687         a File at this point, so morph into one."""
688         self.__class__ = File
689         self._morph()
690         self.clear()
691         return File.rfile(self)
692
693     def get_found_includes(self, env, scanner, path):
694         """If we're looking for included files, it's because this Entry
695         is really supposed to be a File itself."""
696         return self.disambiguate().get_found_includes(env, scanner, path)
697
698     def scanner_key(self):
699         return self.get_suffix()
700
701     def get_contents(self):
702         """Fetch the contents of the entry.
703         
704         Since this should return the real contents from the file
705         system, we check to see into what sort of subclass we should
706         morph this Entry."""
707         if self.isfile():
708             self.__class__ = File
709             self._morph()
710             return self.get_contents()
711         if self.isdir():
712             self.__class__ = Dir
713             self._morph()
714             return self.get_contents()
715         if self.islink():
716             return ''             # avoid errors for dangling symlinks
717         raise AttributeError
718
719     def rel_path(self, other):
720         return self.disambiguate().rel_path(other)
721
722     def exists(self):
723         """Return if the Entry exists.  Check the file system to see
724         what we should turn into first.  Assume a file if there's no
725         directory."""
726         return self.disambiguate().exists()
727
728     def calc_signature(self, calc=None):
729         """Return the Entry's calculated signature.  Check the file
730         system to see what we should turn into first.  Assume a file if
731         there's no directory."""
732         return self.disambiguate().calc_signature(calc)
733
734     def must_be_a_Dir(self):
735         """Called to make sure a Node is a Dir.  Since we're an
736         Entry, we can morph into one."""
737         self.__class__ = Dir
738         self._morph()
739         return self
740
741 # This is for later so we can differentiate between Entry the class and Entry
742 # the method of the FS class.
743 _classEntry = Entry
744
745
746 class LocalFS:
747
748     if SCons.Memoize.use_memoizer:
749         __metaclass__ = SCons.Memoize.Memoized_Metaclass
750     
751     # This class implements an abstraction layer for operations involving
752     # a local file system.  Essentially, this wraps any function in
753     # the os, os.path or shutil modules that we use to actually go do
754     # anything with or to the local file system.
755     #
756     # Note that there's a very good chance we'll refactor this part of
757     # the architecture in some way as we really implement the interface(s)
758     # for remote file system Nodes.  For example, the right architecture
759     # might be to have this be a subclass instead of a base class.
760     # Nevertheless, we're using this as a first step in that direction.
761     #
762     # We're not using chdir() yet because the calling subclass method
763     # needs to use os.chdir() directly to avoid recursion.  Will we
764     # really need this one?
765     #def chdir(self, path):
766     #    return os.chdir(path)
767     def chmod(self, path, mode):
768         return os.chmod(path, mode)
769     def copy2(self, src, dst):
770         return shutil.copy2(src, dst)
771     def exists(self, path):
772         return os.path.exists(path)
773     def getmtime(self, path):
774         return os.path.getmtime(path)
775     def getsize(self, path):
776         return os.path.getsize(path)
777     def isdir(self, path):
778         return os.path.isdir(path)
779     def isfile(self, path):
780         return os.path.isfile(path)
781     def link(self, src, dst):
782         return os.link(src, dst)
783     def lstat(self, path):
784         return os.lstat(path)
785     def listdir(self, path):
786         return os.listdir(path)
787     def makedirs(self, path):
788         return os.makedirs(path)
789     def mkdir(self, path):
790         return os.mkdir(path)
791     def rename(self, old, new):
792         return os.rename(old, new)
793     def stat(self, path):
794         return os.stat(path)
795     def symlink(self, src, dst):
796         return os.symlink(src, dst)
797     def open(self, path):
798         return open(path)
799     def unlink(self, path):
800         return os.unlink(path)
801
802     if hasattr(os, 'symlink'):
803         def islink(self, path):
804             return os.path.islink(path)
805     else:
806         def islink(self, path):
807             return 0                    # no symlinks
808
809 if SCons.Memoize.use_old_memoization():
810     _FSBase = LocalFS
811     class LocalFS(SCons.Memoize.Memoizer, _FSBase):
812         def __init__(self, *args, **kw):
813             apply(_FSBase.__init__, (self,)+args, kw)
814             SCons.Memoize.Memoizer.__init__(self)
815
816
817 #class RemoteFS:
818 #    # Skeleton for the obvious methods we might need from the
819 #    # abstraction layer for a remote filesystem.
820 #    def upload(self, local_src, remote_dst):
821 #        pass
822 #    def download(self, remote_src, local_dst):
823 #        pass
824
825
826 class FS(LocalFS):
827
828     def __init__(self, path = None):
829         """Initialize the Node.FS subsystem.
830
831         The supplied path is the top of the source tree, where we
832         expect to find the top-level build file.  If no path is
833         supplied, the current directory is the default.
834
835         The path argument must be a valid absolute path.
836         """
837         if __debug__: logInstanceCreation(self, 'Node.FS')
838         self.Root = {}
839         self.SConstruct_dir = None
840         self.CachePath = None
841         self.cache_force = None
842         self.cache_show = None
843         self.max_drift = default_max_drift
844
845         if path is None:
846             self.pathTop = os.getcwd()
847         else:
848             self.pathTop = path
849
850         self.Top = self._doLookup(Dir, os.path.normpath(self.pathTop))
851         self.Top.path = '.'
852         self.Top.tpath = '.'
853         self._cwd = self.Top
854
855     def clear_cache(self):
856         "__cache_reset__"
857         pass
858     
859     def set_SConstruct_dir(self, dir):
860         self.SConstruct_dir = dir
861
862     def get_max_drift(self):
863         return self.max_drift
864
865     def set_max_drift(self, max_drift):
866         self.max_drift = max_drift
867
868     def getcwd(self):
869         return self._cwd
870
871     def __checkClass(self, node, klass):
872         if isinstance(node, klass) or klass == Entry:
873             return node
874         if node.__class__ == Entry:
875             node.__class__ = klass
876             node._morph()
877             return node
878         raise TypeError, "Tried to lookup %s '%s' as a %s." % \
879               (node.__class__.__name__, node.path, klass.__name__)
880         
881     def _doLookup(self, fsclass, name, directory = None, create = 1):
882         """This method differs from the File and Dir factory methods in
883         one important way: the meaning of the directory parameter.
884         In this method, if directory is None or not supplied, the supplied
885         name is expected to be an absolute path.  If you try to look up a
886         relative path with directory=None, then an AssertionError will be
887         raised.
888         __cacheable__"""
889
890         if not name:
891             # This is a stupid hack to compensate for the fact that
892             # the POSIX and Win32 versions of os.path.normpath() behave
893             # differently in older versions of Python.  In particular,
894             # in POSIX:
895             #   os.path.normpath('./') == '.'
896             # in Win32
897             #   os.path.normpath('./') == ''
898             #   os.path.normpath('.\\') == ''
899             #
900             # This is a definite bug in the Python library, but we have
901             # to live with it.
902             name = '.'
903         path_orig = string.split(name, os.sep)
904         path_norm = string.split(_my_normcase(name), os.sep)
905
906         first_orig = path_orig.pop(0)   # strip first element
907         first_norm = path_norm.pop(0)   # strip first element
908
909         drive, path_first = os.path.splitdrive(first_orig)
910         if path_first:
911             path_orig = [ path_first, ] + path_orig
912             path_norm = [ _my_normcase(path_first), ] + path_norm
913         else:
914             drive = _my_normcase(drive)
915             # Absolute path
916             try:
917                 directory = self.Root[drive]
918             except KeyError:
919                 if not create:
920                     raise SCons.Errors.UserError
921                 directory = RootDir(drive, self)
922                 self.Root[drive] = directory
923
924         if not path_orig:
925             return directory
926
927         last_orig = path_orig.pop()     # strip last element
928         last_norm = path_norm.pop()     # strip last element
929             
930         # Lookup the directory
931         for orig, norm in map(None, path_orig, path_norm):
932             try:
933                 entries = directory.entries
934             except AttributeError:
935                 # We tried to look up the entry in either an Entry or
936                 # a File.  Give whatever it is a chance to do what's
937                 # appropriate: morph into a Dir or raise an exception.
938                 directory.must_be_a_Dir()
939                 entries = directory.entries
940             try:
941                 directory = entries[norm]
942             except KeyError:
943                 if not create:
944                     raise SCons.Errors.UserError
945
946                 d = Dir(orig, directory, self)
947
948                 # Check the file system (or not, as configured) to make
949                 # sure there isn't already a file there.
950                 d.diskcheck_match()
951
952                 directory.entries[norm] = d
953                 directory.add_wkid(d)
954                 directory = d
955
956         directory.must_be_a_Dir()
957
958         try:
959             e = directory.entries[last_norm]
960         except KeyError:
961             if not create:
962                 raise SCons.Errors.UserError
963
964             result = fsclass(last_orig, directory, self)
965
966             # Check the file system (or not, as configured) to make
967             # sure there isn't already a directory at the path on
968             # disk where we just created a File node, and vice versa.
969             result.diskcheck_match()
970
971             directory.entries[last_norm] = result 
972             directory.add_wkid(result)
973         else:
974             result = self.__checkClass(e, fsclass)
975         return result 
976
977     def _transformPath(self, name, directory):
978         """Take care of setting up the correct top-level directory,
979         usually in preparation for a call to doLookup().
980
981         If the path name is prepended with a '#', then it is unconditionally
982         interpreted as relative to the top-level directory of this FS.
983
984         If directory is None, and name is a relative path,
985         then the same applies.
986         """
987         if name and name[0] == '#':
988             directory = self.Top
989             name = name[1:]
990             if name and (name[0] == os.sep or name[0] == '/'):
991                 # Correct such that '#/foo' is equivalent
992                 # to '#foo'.
993                 name = name[1:]
994             name = os.path.join('.', os.path.normpath(name))
995         elif not directory:
996             directory = self._cwd
997         return (os.path.normpath(name), directory)
998
999     def chdir(self, dir, change_os_dir=0):
1000         """Change the current working directory for lookups.
1001         If change_os_dir is true, we will also change the "real" cwd
1002         to match.
1003         """
1004         curr=self._cwd
1005         try:
1006             if not dir is None:
1007                 self._cwd = dir
1008                 if change_os_dir:
1009                     os.chdir(dir.abspath)
1010         except OSError:
1011             self._cwd = curr
1012             raise
1013
1014     def Entry(self, name, directory = None, create = 1, klass=None):
1015         """Lookup or create a generic Entry node with the specified name.
1016         If the name is a relative path (begins with ./, ../, or a file
1017         name), then it is looked up relative to the supplied directory
1018         node, or to the top level directory of the FS (supplied at
1019         construction time) if no directory is supplied.
1020         """
1021
1022         if not klass:
1023             klass = Entry
1024
1025         if isinstance(name, Base):
1026             return self.__checkClass(name, klass)
1027         else:
1028             if directory and not isinstance(directory, Dir):
1029                 directory = self.Dir(directory)
1030             name, directory = self._transformPath(name, directory)
1031             return self._doLookup(klass, name, directory, create)
1032     
1033     def File(self, name, directory = None, create = 1):
1034         """Lookup or create a File node with the specified name.  If
1035         the name is a relative path (begins with ./, ../, or a file name),
1036         then it is looked up relative to the supplied directory node,
1037         or to the top level directory of the FS (supplied at construction
1038         time) if no directory is supplied.
1039
1040         This method will raise TypeError if a directory is found at the
1041         specified path.
1042         """
1043
1044         return self.Entry(name, directory, create, File)
1045     
1046     def Dir(self, name, directory = None, create = 1):
1047         """Lookup or create a Dir node with the specified name.  If
1048         the name is a relative path (begins with ./, ../, or a file name),
1049         then it is looked up relative to the supplied directory node,
1050         or to the top level directory of the FS (supplied at construction
1051         time) if no directory is supplied.
1052
1053         This method will raise TypeError if a normal file is found at the
1054         specified path.
1055         """
1056
1057         return self.Entry(name, directory, create, Dir)
1058     
1059     def BuildDir(self, build_dir, src_dir, duplicate=1):
1060         """Link the supplied build directory to the source directory
1061         for purposes of building files."""
1062         
1063         if not isinstance(src_dir, SCons.Node.Node):
1064             src_dir = self.Dir(src_dir)
1065         if not isinstance(build_dir, SCons.Node.Node):
1066             build_dir = self.Dir(build_dir)
1067         if src_dir.is_under(build_dir):
1068             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
1069         if build_dir.srcdir:
1070             if build_dir.srcdir == src_dir:
1071                 return # We already did this.
1072             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
1073         build_dir.link(src_dir, duplicate)
1074
1075     def Repository(self, *dirs):
1076         """Specify Repository directories to search."""
1077         for d in dirs:
1078             if not isinstance(d, SCons.Node.Node):
1079                 d = self.Dir(d)
1080             self.Top.addRepository(d)
1081
1082     def Rfindalldirs(self, pathlist, cwd):
1083         """__cacheable__"""
1084         if SCons.Util.is_String(pathlist):
1085             pathlist = string.split(pathlist, os.pathsep)
1086         if not SCons.Util.is_List(pathlist):
1087             pathlist = [pathlist]
1088         result = []
1089         for path in filter(None, pathlist):
1090             if isinstance(path, SCons.Node.Node):
1091                 result.append(path)
1092                 continue
1093             path, dir = self._transformPath(path, cwd)
1094             dir = dir.Dir(path)
1095             result.extend(dir.get_all_rdirs())
1096         return result
1097
1098     def CacheDir(self, path):
1099         self.CachePath = path
1100
1101     def build_dir_target_climb(self, orig, dir, tail):
1102         """Create targets in corresponding build directories
1103
1104         Climb the directory tree, and look up path names
1105         relative to any linked build directories we find.
1106         __cacheable__
1107         """
1108         targets = []
1109         message = None
1110         fmt = "building associated BuildDir targets: %s"
1111         start_dir = dir
1112         while dir:
1113             for bd in dir.build_dirs:
1114                 if start_dir.is_under(bd):
1115                     # If already in the build-dir location, don't reflect
1116                     return [orig], fmt % str(orig)
1117                 p = apply(os.path.join, [bd.path] + tail)
1118                 targets.append(self.Entry(p))
1119             tail = [dir.name] + tail
1120             dir = dir.up()
1121         if targets:
1122             message = fmt % string.join(map(str, targets))
1123         return targets, message
1124
1125 class Dir(Base):
1126     """A class for directories in a file system.
1127     """
1128
1129     def __init__(self, name, directory, fs):
1130         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1131         Base.__init__(self, name, directory, fs)
1132         self._morph()
1133
1134     def _morph(self):
1135         """Turn a file system Node (either a freshly initialized directory
1136         object or a separate Entry object) into a proper directory object.
1137
1138         Set up this directory's entries and hook it into the file
1139         system tree.  Specify that directories (this Node) don't use
1140         signatures for calculating whether they're current.
1141         __cache_reset__"""
1142
1143         self.repositories = []
1144         self.srcdir = None
1145
1146         self.entries = {}
1147         self.entries['.'] = self
1148         self.entries['..'] = self.dir
1149         self.cwd = self
1150         self.searched = 0
1151         self._sconsign = None
1152         self.build_dirs = []
1153
1154         # Don't just reset the executor, replace its action list,
1155         # because it might have some pre-or post-actions that need to
1156         # be preserved.
1157         self.builder = get_MkdirBuilder()
1158         self.get_executor().set_action_list(self.builder.action)
1159
1160     def diskcheck_match(self):
1161         diskcheck_match(self, self.fs.isfile,
1162                            "File %s found where directory expected.")
1163
1164     def disambiguate(self):
1165         return self
1166
1167     def __clearRepositoryCache(self, duplicate=None):
1168         """Called when we change the repository(ies) for a directory.
1169         This clears any cached information that is invalidated by changing
1170         the repository."""
1171
1172         for node in self.entries.values():
1173             if node != self.dir:
1174                 if node != self and isinstance(node, Dir):
1175                     node.__clearRepositoryCache(duplicate)
1176                 else:
1177                     node.clear()
1178                     try:
1179                         del node._srcreps
1180                     except AttributeError:
1181                         pass
1182                     if duplicate != None:
1183                         node.duplicate=duplicate
1184     
1185     def __resetDuplicate(self, node):
1186         if node != self:
1187             node.duplicate = node.get_dir().duplicate
1188
1189     def Entry(self, name):
1190         """Create an entry node named 'name' relative to this directory."""
1191         return self.fs.Entry(name, self)
1192
1193     def Dir(self, name):
1194         """Create a directory node named 'name' relative to this directory."""
1195         return self.fs.Dir(name, self)
1196
1197     def File(self, name):
1198         """Create a file node named 'name' relative to this directory."""
1199         return self.fs.File(name, self)
1200
1201     def link(self, srcdir, duplicate):
1202         """Set this directory as the build directory for the
1203         supplied source directory."""
1204         self.srcdir = srcdir
1205         self.duplicate = duplicate
1206         self.__clearRepositoryCache(duplicate)
1207         srcdir.build_dirs.append(self)
1208
1209     def getRepositories(self):
1210         """Returns a list of repositories for this directory.
1211         __cacheable__"""
1212         if self.srcdir and not self.duplicate:
1213             return self.srcdir.get_all_rdirs() + self.repositories
1214         return self.repositories
1215
1216     def get_all_rdirs(self):
1217         """__cacheable__"""
1218         result = [self]
1219         fname = '.'
1220         dir = self
1221         while dir:
1222             for rep in dir.getRepositories():
1223                 result.append(rep.Dir(fname))
1224             fname = dir.name + os.sep + fname
1225             dir = dir.up()
1226         return result
1227
1228     def addRepository(self, dir):
1229         if dir != self and not dir in self.repositories:
1230             self.repositories.append(dir)
1231             dir.tpath = '.'
1232             self.__clearRepositoryCache()
1233
1234     def up(self):
1235         return self.entries['..']
1236
1237     def rel_path(self, other):
1238         """Return a path to "other" relative to this directory.
1239         __cacheable__"""
1240         if isinstance(other, Dir):
1241             name = []
1242         else:
1243             try:
1244                 name = [other.name]
1245                 other = other.dir
1246             except AttributeError:
1247                 return str(other)
1248         if self is other:
1249             return name and name[0] or '.'
1250         i = 0
1251         for x, y in map(None, self.path_elements, other.path_elements):
1252             if not x is y:
1253                 break
1254             i = i + 1
1255         path_elems = ['..']*(len(self.path_elements)-i) \
1256                    + map(lambda n: n.name, other.path_elements[i:]) \
1257                    + name
1258              
1259         return string.join(path_elems, os.sep)
1260
1261     def scan(self):
1262         if not self.implicit is None:
1263             return
1264         self.implicit = []
1265         self.implicit_dict = {}
1266         self._children_reset()
1267
1268         dont_scan = lambda k: k not in ['.', '..', '.sconsign']
1269         deps = filter(dont_scan, self.entries.keys())
1270         # keys() is going to give back the entries in an internal,
1271         # unsorted order.  Sort 'em so the order is deterministic.
1272         deps.sort()
1273         entries = map(lambda n, e=self.entries: e[n], deps)
1274
1275         self._add_child(self.implicit, self.implicit_dict, entries)
1276
1277     def get_found_includes(self, env, scanner, path):
1278         """Return the included implicit dependencies in this file.
1279         Cache results so we only scan the file once per path
1280         regardless of how many times this information is requested.
1281         __cacheable__"""
1282         if not scanner:
1283             return []
1284         # Clear cached info for this Node.  If we already visited this
1285         # directory on our walk down the tree (because we didn't know at
1286         # that point it was being used as the source for another Node)
1287         # then we may have calculated build signature before realizing
1288         # we had to scan the disk.  Now that we have to, though, we need
1289         # to invalidate the old calculated signature so that any node
1290         # dependent on our directory structure gets one that includes
1291         # info about everything on disk.
1292         self.clear()
1293         return scanner(self, env, path)
1294
1295     def build(self, **kw):
1296         """A null "builder" for directories."""
1297         global MkdirBuilder
1298         if not self.builder is MkdirBuilder:
1299             apply(SCons.Node.Node.build, [self,], kw)
1300
1301     def _create(self):
1302         """Create this directory, silently and without worrying about
1303         whether the builder is the default or not."""
1304         listDirs = []
1305         parent = self
1306         while parent:
1307             if parent.exists():
1308                 break
1309             listDirs.append(parent)
1310             p = parent.up()
1311             if p is None:
1312                 raise SCons.Errors.StopError, parent.path
1313             parent = p
1314         listDirs.reverse()
1315         for dirnode in listDirs:
1316             try:
1317                 # Don't call dirnode.build(), call the base Node method
1318                 # directly because we definitely *must* create this
1319                 # directory.  The dirnode.build() method will suppress
1320                 # the build if it's the default builder.
1321                 SCons.Node.Node.build(dirnode)
1322                 dirnode.get_executor().nullify()
1323                 # The build() action may or may not have actually
1324                 # created the directory, depending on whether the -n
1325                 # option was used or not.  Delete the _exists and
1326                 # _rexists attributes so they can be reevaluated.
1327                 dirnode.clear()
1328             except OSError:
1329                 pass
1330
1331     def multiple_side_effect_has_builder(self):
1332         global MkdirBuilder
1333         return not self.builder is MkdirBuilder and self.has_builder()
1334
1335     def alter_targets(self):
1336         """Return any corresponding targets in a build directory.
1337         """
1338         return self.fs.build_dir_target_climb(self, self, [])
1339
1340     def scanner_key(self):
1341         """A directory does not get scanned."""
1342         return None
1343
1344     def get_contents(self):
1345         """Return aggregate contents of all our children."""
1346         contents = cStringIO.StringIO()
1347         for kid in self.children():
1348             contents.write(kid.get_contents())
1349         return contents.getvalue()
1350
1351     def prepare(self):
1352         pass
1353
1354     def do_duplicate(self, src):
1355         pass
1356
1357     def current(self, calc=None):
1358         """If all of our children were up-to-date, then this
1359         directory was up-to-date, too."""
1360         if not self.builder is MkdirBuilder and not self.exists():
1361             return 0
1362         state = 0
1363         for kid in self.children():
1364             s = kid.get_state()
1365             if s and (not state or s > state):
1366                 state = s
1367         import SCons.Node
1368         if state == 0 or state == SCons.Node.up_to_date:
1369             return 1
1370         else:
1371             return 0
1372
1373     def rdir(self):
1374         "__cacheable__"
1375         if not self.exists():
1376             norm_name = _my_normcase(self.name)
1377             for dir in self.dir.get_all_rdirs():
1378                 try: node = dir.entries[norm_name]
1379                 except KeyError: node = dir.dir_on_disk(self.name)
1380                 if node and node.exists() and \
1381                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1382                         return node
1383         return self
1384
1385     def sconsign(self):
1386         """Return the .sconsign file info for this directory,
1387         creating it first if necessary."""
1388         if not self._sconsign:
1389             import SCons.SConsign
1390             self._sconsign = SCons.SConsign.ForDirectory(self)
1391         return self._sconsign
1392
1393     def srcnode(self):
1394         """Dir has a special need for srcnode()...if we
1395         have a srcdir attribute set, then that *is* our srcnode."""
1396         if self.srcdir:
1397             return self.srcdir
1398         return Base.srcnode(self)
1399
1400     def get_timestamp(self):
1401         """Return the latest timestamp from among our children"""
1402         stamp = 0
1403         for kid in self.children():
1404             if kid.get_timestamp() > stamp:
1405                 stamp = kid.get_timestamp()
1406         return stamp
1407
1408     def entry_abspath(self, name):
1409         return self.abspath + os.sep + name
1410
1411     def entry_path(self, name):
1412         return self.path + os.sep + name
1413
1414     def entry_tpath(self, name):
1415         return self.tpath + os.sep + name
1416
1417     def must_be_a_Dir(self):
1418         """Called to make sure a Node is a Dir.  Since we're already
1419         one, this is a no-op for us."""
1420         return self
1421
1422     def entry_exists_on_disk(self, name):
1423         """__cacheable__"""
1424         return self.fs.exists(self.entry_abspath(name))
1425
1426     def srcdir_list(self):
1427         """__cacheable__"""
1428         result = []
1429
1430         dirname = '.'
1431         dir = self
1432         while dir:
1433             if dir.srcdir:
1434                 d = dir.srcdir.Dir(dirname)
1435                 if d.is_under(dir):
1436                     # Shouldn't source from something in the build path:
1437                     # build_dir is probably under src_dir, in which case
1438                     # we are reflecting.
1439                     break
1440                 result.append(d)
1441             dirname = dir.name + os.sep + dirname
1442             dir = dir.up()
1443
1444         return result
1445
1446     def srcdir_duplicate(self, name):
1447         for dir in self.srcdir_list():
1448             if dir.entry_exists_on_disk(name):
1449                 srcnode = dir.File(name)
1450                 if self.duplicate:
1451                     node = self.File(name)
1452                     node.do_duplicate(srcnode)
1453                     return node
1454                 else:
1455                     return srcnode
1456         return None
1457
1458     def srcdir_find_file(self, filename):
1459         """__cacheable__"""
1460         def func(node):
1461             if (isinstance(node, File) or isinstance(node, Entry)) and \
1462                (node.is_derived() or node.is_pseudo_derived() or node.exists()):
1463                     return node
1464             return None
1465
1466         norm_name = _my_normcase(filename)
1467
1468         for rdir in self.get_all_rdirs():
1469             try: node = rdir.entries[norm_name]
1470             except KeyError: node = rdir.file_on_disk(filename)
1471             else: node = func(node)
1472             if node:
1473                 return node, self
1474
1475         for srcdir in self.srcdir_list():
1476             for rdir in srcdir.get_all_rdirs():
1477                 try: node = rdir.entries[norm_name]
1478                 except KeyError: node = rdir.file_on_disk(filename)
1479                 else: node = func(node)
1480                 if node:
1481                     return File(filename, self, self.fs), srcdir
1482
1483         return None, None
1484
1485     def dir_on_disk(self, name):
1486         if self.entry_exists_on_disk(name):
1487             try: return self.Dir(name)
1488             except TypeError: pass
1489         return None
1490
1491     def file_on_disk(self, name):
1492         if self.entry_exists_on_disk(name) or \
1493            diskcheck_rcs(self, name) or \
1494            diskcheck_sccs(self, name):
1495             try: return self.File(name)
1496             except TypeError: pass
1497         return self.srcdir_duplicate(name)
1498
1499 class RootDir(Dir):
1500     """A class for the root directory of a file system.
1501
1502     This is the same as a Dir class, except that the path separator
1503     ('/' or '\\') is actually part of the name, so we don't need to
1504     add a separator when creating the path names of entries within
1505     this directory.
1506     """
1507     def __init__(self, name, fs):
1508         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1509         # We're going to be our own parent directory (".." entry and .dir
1510         # attribute) so we have to set up some values so Base.__init__()
1511         # won't gag won't it calls some of our methods.
1512         self.abspath = ''
1513         self.path = ''
1514         self.tpath = ''
1515         self.path_elements = []
1516         self.duplicate = 0
1517         Base.__init__(self, name, self, fs)
1518
1519         # Now set our paths to what we really want them to be: the
1520         # initial drive letter (the name) plus the directory separator.
1521         self.abspath = name + os.sep
1522         self.path = name + os.sep
1523         self.tpath = name + os.sep
1524         self._morph()
1525
1526     def __str__(self):
1527         return self.abspath
1528
1529     def entry_abspath(self, name):
1530         return self.abspath + name
1531
1532     def entry_path(self, name):
1533         return self.path + name
1534
1535     def entry_tpath(self, name):
1536         return self.tpath + name
1537
1538     def is_under(self, dir):
1539         if self is dir:
1540             return 1
1541         else:
1542             return 0
1543
1544     def up(self):
1545         return None
1546
1547     def get_dir(self):
1548         return None
1549
1550     def src_builder(self):
1551         return _null
1552
1553 class NodeInfo(SCons.Node.NodeInfo):
1554     # The bsig attributes needs to stay here, if it's initialized in
1555     # __init__() then the assignment seems to overwrite any values
1556     # unpickled from .sconsign files.
1557     bsig = None
1558     def __cmp__(self, other):
1559         return cmp(self.bsig, other.bsig)
1560     def update(self, node):
1561         self.timestamp = node.get_timestamp()
1562         self.size = node.getsize()
1563
1564 class BuildInfo(SCons.Node.BuildInfo):
1565     def __init__(self, node):
1566         SCons.Node.BuildInfo.__init__(self, node)
1567         self.node = node
1568     def convert_to_sconsign(self):
1569         """Convert this BuildInfo object for writing to a .sconsign file
1570
1571         We hung onto the node that we refer to so that we can translate
1572         the lists of bsources, bdepends and bimplicit Nodes into strings
1573         relative to the node, but we don't want to write out that Node
1574         itself to the .sconsign file, so we delete the attribute in
1575         preparation.
1576         """
1577         rel_path = self.node.rel_path
1578         delattr(self, 'node')
1579         for attr in ['bsources', 'bdepends', 'bimplicit']:
1580             try:
1581                 val = getattr(self, attr)
1582             except AttributeError:
1583                 pass
1584             else:
1585                 setattr(self, attr, map(rel_path, val))
1586     def convert_from_sconsign(self, dir, name):
1587         """Convert a newly-read BuildInfo object for in-SCons use
1588
1589         An on-disk BuildInfo comes without a reference to the node
1590         for which it's intended, so we have to convert the arguments
1591         and add back a self.node attribute.  The bsources, bdepends and
1592         bimplicit lists all come from disk as paths relative to that node,
1593         so convert them to actual Nodes for use by the rest of SCons.
1594         """
1595         self.node = dir.Entry(name)
1596         Entry_func = self.node.dir.Entry
1597         for attr in ['bsources', 'bdepends', 'bimplicit']:
1598             try:
1599                 val = getattr(self, attr)
1600             except AttributeError:
1601                 pass
1602             else:
1603                 setattr(self, attr, map(Entry_func, val))
1604
1605 class File(Base):
1606     """A class for files in a file system.
1607     """
1608     def diskcheck_match(self):
1609         diskcheck_match(self, self.fs.isdir,
1610                            "Directory %s found where file expected.")
1611
1612     def __init__(self, name, directory, fs):
1613         if __debug__: logInstanceCreation(self, 'Node.FS.File')
1614         Base.__init__(self, name, directory, fs)
1615         self._morph()
1616
1617     def Entry(self, name):
1618         """Create an entry node named 'name' relative to
1619         the SConscript directory of this file."""
1620         return self.fs.Entry(name, self.cwd)
1621
1622     def Dir(self, name):
1623         """Create a directory node named 'name' relative to
1624         the SConscript directory of this file."""
1625         return self.fs.Dir(name, self.cwd)
1626
1627     def Dirs(self, pathlist):
1628         """Create a list of directories relative to the SConscript
1629         directory of this file."""
1630         return map(lambda p, s=self: s.Dir(p), pathlist)
1631
1632     def File(self, name):
1633         """Create a file node named 'name' relative to
1634         the SConscript directory of this file."""
1635         return self.fs.File(name, self.cwd)
1636
1637     def RDirs(self, pathlist):
1638         """Search for a list of directories in the Repository list."""
1639         return self.fs.Rfindalldirs(pathlist, self.cwd)
1640
1641     def _morph(self):
1642         """Turn a file system node into a File object.  __cache_reset__"""
1643         self.scanner_paths = {}
1644         if not hasattr(self, '_local'):
1645             self._local = 0
1646
1647     def disambiguate(self):
1648         return self
1649
1650     def scanner_key(self):
1651         return self.get_suffix()
1652
1653     def get_contents(self):
1654         if not self.rexists():
1655             return ''
1656         return open(self.rfile().abspath, "rb").read()
1657
1658     def get_timestamp(self):
1659         if self.rexists():
1660             return self.rfile().getmtime()
1661         else:
1662             return 0
1663
1664     def store_info(self, obj):
1665         # Merge our build information into the already-stored entry.
1666         # This accomodates "chained builds" where a file that's a target
1667         # in one build (SConstruct file) is a source in a different build.
1668         # See test/chained-build.py for the use case.
1669         entry = self.get_stored_info()
1670         entry.merge(obj)
1671         self.dir.sconsign().set_entry(self.name, entry)
1672
1673     def get_stored_info(self):
1674         "__cacheable__"
1675         try:
1676             stored = self.dir.sconsign().get_entry(self.name)
1677         except (KeyError, OSError):
1678             return self.new_binfo()
1679         else:
1680             if not hasattr(stored, 'ninfo'):
1681                 # Transition:  The .sconsign file entry has no NodeInfo
1682                 # object, which means it's a slightly older BuildInfo.
1683                 # Copy over the relevant attributes.
1684                 ninfo = stored.ninfo = self.new_ninfo()
1685                 for attr in ninfo.__dict__.keys():
1686                     try:
1687                         setattr(ninfo, attr, getattr(stored, attr))
1688                     except AttributeError:
1689                         pass
1690             return stored
1691
1692     def get_stored_implicit(self):
1693         binfo = self.get_stored_info()
1694         try: return binfo.bimplicit
1695         except AttributeError: return None
1696
1697     def rel_path(self, other):
1698         return self.dir.rel_path(other)
1699
1700     def get_found_includes(self, env, scanner, path):
1701         """Return the included implicit dependencies in this file.
1702         Cache results so we only scan the file once per path
1703         regardless of how many times this information is requested.
1704         __cacheable__"""
1705         if not scanner:
1706             return []
1707         return scanner(self, env, path)
1708
1709     def _createDir(self):
1710         # ensure that the directories for this node are
1711         # created.
1712         self.dir._create()
1713
1714     def retrieve_from_cache(self):
1715         """Try to retrieve the node's content from a cache
1716
1717         This method is called from multiple threads in a parallel build,
1718         so only do thread safe stuff here. Do thread unsafe stuff in
1719         built().
1720
1721         Note that there's a special trick here with the execute flag
1722         (one that's not normally done for other actions).  Basically
1723         if the user requested a noexec (-n) build, then
1724         SCons.Action.execute_actions is set to 0 and when any action
1725         is called, it does its showing but then just returns zero
1726         instead of actually calling the action execution operation.
1727         The problem for caching is that if the file does NOT exist in
1728         cache then the CacheRetrieveString won't return anything to
1729         show for the task, but the Action.__call__ won't call
1730         CacheRetrieveFunc; instead it just returns zero, which makes
1731         the code below think that the file *was* successfully
1732         retrieved from the cache, therefore it doesn't do any
1733         subsequent building.  However, the CacheRetrieveString didn't
1734         print anything because it didn't actually exist in the cache,
1735         and no more build actions will be performed, so the user just
1736         sees nothing.  The fix is to tell Action.__call__ to always
1737         execute the CacheRetrieveFunc and then have the latter
1738         explicitly check SCons.Action.execute_actions itself.
1739
1740         Returns true iff the node was successfully retrieved.
1741         """
1742         b = self.is_derived()
1743         if not b and not self.has_src_builder():
1744             return None
1745         if b and self.fs.CachePath:
1746             if self.fs.cache_show:
1747                 if CacheRetrieveSilent(self, [], None, execute=1) == 0:
1748                     self.build(presub=0, execute=0)
1749                     return 1
1750             elif CacheRetrieve(self, [], None, execute=1) == 0:
1751                 return 1
1752         return None
1753
1754     def built(self):
1755         """Called just after this node is successfully built.
1756         __cache_reset__"""
1757         # Push this file out to cache before the superclass Node.built()
1758         # method has a chance to clear the build signature, which it
1759         # will do if this file has a source scanner.
1760         if self.fs.CachePath and self.exists():
1761             CachePush(self, [], None)
1762         self.fs.clear_cache()
1763         SCons.Node.Node.built(self)
1764
1765     def visited(self):
1766         if self.fs.CachePath and self.fs.cache_force and self.exists():
1767             CachePush(self, None, None)
1768
1769     def has_src_builder(self):
1770         """Return whether this Node has a source builder or not.
1771
1772         If this Node doesn't have an explicit source code builder, this
1773         is where we figure out, on the fly, if there's a transparent
1774         source code builder for it.
1775
1776         Note that if we found a source builder, we also set the
1777         self.builder attribute, so that all of the methods that actually
1778         *build* this file don't have to do anything different.
1779         """
1780         try:
1781             scb = self.sbuilder
1782         except AttributeError:
1783             if self.rexists():
1784                 scb = None
1785             else:
1786                 scb = self.dir.src_builder()
1787                 if scb is _null:
1788                     if diskcheck_sccs(self.dir, self.name):
1789                         scb = get_DefaultSCCSBuilder()
1790                     elif diskcheck_rcs(self.dir, self.name):
1791                         scb = get_DefaultRCSBuilder()
1792                     else:
1793                         scb = None
1794                 if scb is not None:
1795                     self.builder_set(scb)
1796             self.sbuilder = scb
1797         return not scb is None
1798
1799     def alter_targets(self):
1800         """Return any corresponding targets in a build directory.
1801         """
1802         if self.is_derived():
1803             return [], None
1804         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
1805
1806     def is_pseudo_derived(self):
1807         "__cacheable__"
1808         return self.has_src_builder()
1809
1810     def _rmv_existing(self):
1811         '__cache_reset__'
1812         Unlink(self, [], None)
1813         
1814     def prepare(self):
1815         """Prepare for this file to be created."""
1816         SCons.Node.Node.prepare(self)
1817
1818         if self.get_state() != SCons.Node.up_to_date:
1819             if self.exists():
1820                 if self.is_derived() and not self.precious:
1821                     self._rmv_existing()
1822             else:
1823                 try:
1824                     self._createDir()
1825                 except SCons.Errors.StopError, drive:
1826                     desc = "No drive `%s' for target `%s'." % (drive, self)
1827                     raise SCons.Errors.StopError, desc
1828
1829     def remove(self):
1830         """Remove this file."""
1831         if self.exists() or self.islink():
1832             self.fs.unlink(self.path)
1833             return 1
1834         return None
1835
1836     def do_duplicate(self, src):
1837         self._createDir()
1838         try:
1839             Unlink(self, None, None)
1840         except SCons.Errors.BuildError:
1841             pass
1842         try:
1843             Link(self, src, None)
1844         except SCons.Errors.BuildError, e:
1845             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
1846             raise SCons.Errors.StopError, desc
1847         self.linked = 1
1848         # The Link() action may or may not have actually
1849         # created the file, depending on whether the -n
1850         # option was used or not.  Delete the _exists and
1851         # _rexists attributes so they can be reevaluated.
1852         self.clear()
1853
1854     def exists(self):
1855         "__cacheable__"
1856         # Duplicate from source path if we are set up to do this.
1857         if self.duplicate and not self.is_derived() and not self.linked:
1858             src=self.srcnode()
1859             if src is self:
1860                 return Base.exists(self)
1861             src = src.rfile()
1862             if src.abspath != self.abspath and src.exists():
1863                 self.do_duplicate(src)
1864         return Base.exists(self)
1865
1866     #
1867     # SIGNATURE SUBSYSTEM
1868     #
1869
1870     def new_binfo(self):
1871         return BuildInfo(self)
1872
1873     def new_ninfo(self):
1874         ninfo = NodeInfo()
1875         ninfo.update(self)
1876         return ninfo
1877
1878     def get_csig(self, calc=None):
1879         """
1880         Generate a node's content signature, the digested signature
1881         of its content.
1882
1883         node - the node
1884         cache - alternate node to use for the signature cache
1885         returns - the content signature
1886         """
1887         try:
1888             return self.binfo.ninfo.csig
1889         except AttributeError:
1890             pass
1891
1892         if calc is None:
1893             calc = self.calculator()
1894
1895         max_drift = self.fs.max_drift
1896         mtime = self.get_timestamp()
1897         use_stored = max_drift >= 0 and (time.time() - mtime) > max_drift
1898
1899         csig = None
1900         if use_stored:
1901             old = self.get_stored_info().ninfo
1902             try:
1903                 if old.timestamp and old.csig and old.timestamp == mtime:
1904                     csig = old.csig
1905             except AttributeError:
1906                 pass
1907         if csig is None:
1908             csig = calc.module.signature(self)
1909
1910         binfo = self.get_binfo()
1911         ninfo = binfo.ninfo
1912         ninfo.csig = csig
1913         ninfo.update(self)
1914
1915         if use_stored:
1916             self.store_info(binfo)
1917
1918         return csig
1919
1920     #
1921     #
1922     #
1923
1924     def current(self, calc=None):
1925         self.binfo = self.gen_binfo(calc)
1926         return self._cur2()
1927     def _cur2(self):
1928         "__cacheable__"
1929         if self.always_build:
1930             return None
1931         if not self.exists():
1932             # The file doesn't exist locally...
1933             r = self.rfile()
1934             if r != self:
1935                 # ...but there is one in a Repository...
1936                 old = r.get_stored_info()
1937                 new = self.get_binfo()
1938                 if new == old:
1939                     # ...and it's even up-to-date...
1940                     if self._local:
1941                         # ...and they'd like a local copy.
1942                         LocalCopy(self, r, None)
1943                         self.store_info(new)
1944                     return 1
1945             return None
1946         else:
1947             old = self.get_stored_info()
1948             new = self.get_binfo()
1949             return (new == old)
1950
1951     def rfile(self):
1952         "__cacheable__"
1953         if not self.exists():
1954             norm_name = _my_normcase(self.name)
1955             for dir in self.dir.get_all_rdirs():
1956                 try: node = dir.entries[norm_name]
1957                 except KeyError: node = dir.file_on_disk(self.name)
1958                 if node and node.exists() and \
1959                    (isinstance(node, File) or isinstance(node, Entry) \
1960                     or not node.is_derived()):
1961                         return node
1962         return self
1963
1964     def rstr(self):
1965         return str(self.rfile())
1966
1967     def cachepath(self):
1968         if not self.fs.CachePath:
1969             return None, None
1970         ninfo = self.get_binfo().ninfo
1971         if not hasattr(ninfo, 'bsig'):
1972             raise SCons.Errors.InternalError, "cachepath(%s) found no bsig" % self.path
1973         elif ninfo.bsig is None:
1974             raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1975         # Add the path to the cache signature, because multiple
1976         # targets built by the same action will all have the same
1977         # build signature, and we have to differentiate them somehow.
1978         cache_sig = SCons.Sig.MD5.collect([ninfo.bsig, self.path])
1979         subdir = string.upper(cache_sig[0])
1980         dir = os.path.join(self.fs.CachePath, subdir)
1981         return dir, os.path.join(dir, cache_sig)
1982
1983     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
1984         return self.dir.File(prefix + splitext(self.name)[0] + suffix)
1985
1986     def must_be_a_Dir(self):
1987         """Called to make sure a Node is a Dir.  Since we're already a
1988         File, this is a TypeError..."""
1989         raise TypeError, "Tried to lookup File '%s' as a Dir." % self.path
1990
1991 default_fs = None
1992
1993 def find_file(filename, paths, verbose=None):
1994     """
1995     find_file(str, [Dir()]) -> [nodes]
1996
1997     filename - a filename to find
1998     paths - a list of directory path *nodes* to search in.  Can be
1999             represented as a list, a tuple, or a callable that is
2000             called with no arguments and returns the list or tuple.
2001
2002     returns - the node created from the found file.
2003
2004     Find a node corresponding to either a derived file or a file
2005     that exists already.
2006
2007     Only the first file found is returned, and none is returned
2008     if no file is found.
2009     __cacheable__
2010     """
2011     if verbose:
2012         if not SCons.Util.is_String(verbose):
2013             verbose = "find_file"
2014         if not callable(verbose):
2015             verbose = '  %s: ' % verbose
2016             verbose = lambda s, v=verbose: sys.stdout.write(v + s)
2017     else:
2018         verbose = lambda x: x
2019
2020     if callable(paths):
2021         paths = paths()
2022
2023     # Give Entries a chance to morph into Dirs.
2024     paths = map(lambda p: p.must_be_a_Dir(), paths)
2025
2026     filedir, filename = os.path.split(filename)
2027     if filedir:
2028         def filedir_lookup(p, fd=filedir):
2029             try:
2030                 return p.Dir(fd)
2031             except TypeError:
2032                 # We tried to look up a Dir, but it seems there's already
2033                 # a File (or something else) there.  No big.
2034                 return None
2035         paths = filter(None, map(filedir_lookup, paths))
2036
2037     for dir in paths:
2038         verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
2039         node, d = dir.srcdir_find_file(filename)
2040         if node:
2041             verbose("... FOUND '%s' in '%s'\n" % (filename, d))
2042             return node
2043     return None
2044
2045 def find_files(filenames, paths):
2046     """
2047     find_files([str], [Dir()]) -> [nodes]
2048
2049     filenames - a list of filenames to find
2050     paths - a list of directory path *nodes* to search in
2051
2052     returns - the nodes created from the found files.
2053
2054     Finds nodes corresponding to either derived files or files
2055     that exist already.
2056
2057     Only the first file found is returned for each filename,
2058     and any files that aren't found are ignored.
2059     """
2060     nodes = map(lambda x, paths=paths: find_file(x, paths), filenames)
2061     return filter(None, nodes)