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