Windows NT portability fixes for tests.
[scons.git] / src / engine / SCons / Node / FS.py
1 """scons.Node.FS
2
3 File system nodes.
4
5 This initializes a "default_fs" Node with an FS at the current directory
6 for its own purposes, and for use by scripts or modules looking for the
7 canonical default.
8
9 """
10
11 #
12 # Copyright (c) 2001 Steven Knight
13 #
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 #
33
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
35
36 import os
37 import os.path
38 import types
39 import SCons.Node
40 from UserDict import UserDict
41 import sys
42 from SCons.Errors import UserError
43
44 try:
45     import os
46     file_link = os.link
47 except AttributeError:
48     import shutil
49     import stat
50     def file_link(src, dest):
51         shutil.copyfile(src, dest)
52         st=os.stat(src)
53         os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
54
55 class PathName:
56     """This is a string like object with limited capabilities (i.e.,
57     cannot always be used interchangeably with strings).  This class
58     is used by PathDict to manage case-insensitive path names.  It preserves
59     the case of the string with which it was created, but on OS's with
60     case insensitive paths, it will hash equal to any case of the same
61     path when placed in a dictionary."""
62
63     try:
64         convert_path = unicode
65     except NameError:
66         convert_path = str
67
68     def __init__(self, path_name=''):
69         self.data = PathName.convert_path(path_name)
70         self.norm_path = os.path.normcase(self.data)
71
72     def __hash__(self):
73         return hash(self.norm_path)
74     def __cmp__(self, other):
75         return cmp(self.norm_path,
76                    os.path.normcase(PathName.convert_path(other)))
77     def __rcmp__(self, other):
78         return cmp(os.path.normcase(PathName.convert_path(other)),
79                    self.norm_path)
80     def __str__(self):
81         return str(self.data)
82     def __repr__(self):
83         return repr(self.data)
84
85 class PathDict(UserDict):
86     """This is a dictionary-like class meant to hold items keyed
87     by path name.  The difference between this class and a normal
88     dictionary is that string or unicode keys will act differently
89     on OS's that have case-insensitive path names.  Specifically
90     string or unicode keys of different case will be treated as
91     equal on the OS's.
92
93     All keys are implicitly converted to PathName objects before
94     insertion into the dictionary."""
95
96     def __init__(self, initdict = {}):
97         UserDict.__init__(self, initdict)
98         old_dict = self.data
99         self.data = {}
100         for key, val in old_dict.items():
101             self.data[PathName(key)] = val
102
103     def __setitem__(self, key, val):
104         self.data[PathName(key)] = val
105
106     def __getitem__(self, key):
107         return self.data[PathName(key)]
108
109     def __delitem__(self, key):
110         del(self.data[PathName(key)])
111
112     def setdefault(self, key, value):
113         try:
114             return self.data[PathName(key)]
115         except KeyError:
116             self.data[PathName(key)] = value
117             return value
118
119 class FS:
120     def __init__(self, path = None):
121         """Initialize the Node.FS subsystem.
122
123         The supplied path is the top of the source tree, where we
124         expect to find the top-level build file.  If no path is
125         supplied, the current directory is the default.
126
127         The path argument must be a valid absolute path.
128         """
129         if path == None:
130             self.pathTop = os.getcwd()
131         else:
132             self.pathTop = path
133         self.Root = PathDict()
134         self.Top = None
135
136     def set_toplevel_dir(self, path):
137         assert not self.Top, "You can only set the top-level path on an FS object that has not had its File, Dir, or Entry methods called yet."
138         self.pathTop = path
139         
140     def __setTopLevelDir(self):
141         if not self.Top:
142             self.Top = self.__doLookup(Dir, self.pathTop)
143             self.Top.path = '.'
144             self.Top.srcpath = '.'
145             self.Top.path_ = os.path.join('.', '')
146             self._cwd = self.Top
147         
148     def __hash__(self):
149         self.__setTopLevelDir()
150         return hash(self.Top)
151
152     def __cmp__(self, other):
153         self.__setTopLevelDir()
154         if isinstance(other, FS):
155             other.__setTopLevelDir()
156         return cmp(self.__dict__, other.__dict__)
157
158     def getcwd(self):
159         self.__setTopLevelDir()
160         return self._cwd
161
162     def __doLookup(self, fsclass, name, directory=None):
163         """This method differs from the File and Dir factory methods in
164         one important way: the meaning of the directory parameter.
165         In this method, if directory is None or not supplied, the supplied
166         name is expected to be an absolute path.  If you try to look up a
167         relative path with directory=None, then an AssertionError will be
168         raised."""
169
170         head, tail = os.path.split(os.path.normpath(name))
171         if not tail:
172             # We have reached something that looks like a root
173             # of an absolute path.  What we do here is a little
174             # weird.  If we are on a UNIX system, everything is
175             # well and good, just return the root node.
176             #
177             # On DOS/Win32 things are strange, since a path
178             # starting with a slash is not technically an
179             # absolute path, but a path relative to the
180             # current drive.  Therefore if we get a path like
181             # that, we will return the root Node of the
182             # directory parameter.  If the directory parameter is
183             # None, raise an exception.
184
185             drive, tail = os.path.splitdrive(head)
186             #if sys.platform is 'win32' and not drive:
187             #    if not directory:
188             #        raise OSError, 'No drive letter supplied for absolute path.'
189             #    return directory.root()
190             dir = Dir(tail)
191             dir.path = drive + dir.path
192             dir.path_ = drive + dir.path_
193             dir.abspath = drive + dir.abspath
194             dir.abspath_ = drive + dir.abspath_
195             dir.srcpath = dir.path
196             return self.Root.setdefault(drive, dir)
197         if head:
198             # Recursively look up our parent directories.
199             directory = self.__doLookup(Dir, head, directory)
200         else:
201             # This path looks like a relative path.  No leading slash or drive
202             # letter.  Therefore, we will look up this path relative to the
203             # supplied top-level directory.
204             assert directory, "Tried to lookup a node by relative path with no top-level directory supplied."
205         ret = directory.entries.setdefault(tail, fsclass(tail, directory))
206         if fsclass.__name__ == 'Entry':
207             # If they were looking up a generic entry, then
208             # whatever came back is all right.
209             return ret
210         if ret.__class__.__name__ == 'Entry':
211             # They were looking up a File or Dir but found a
212             # generic entry.  Transform the node.
213             ret.__class__ = fsclass
214             ret._morph()
215             return ret
216         if not isinstance(ret, fsclass):
217             raise TypeError, "Tried to lookup %s '%s' as a %s." % \
218                         (ret.__class__.__name__, str(ret), fsclass.__name__)
219         return ret
220
221     def __transformPath(self, name, directory):
222         """Take care of setting up the correct top-level directory,
223         usually in preparation for a call to doLookup().
224
225         If the path name is prepended with a '#', then it is unconditionally
226         interpreted as relative to the top-level directory of this FS.
227
228         If directory is None, and name is a relative path,
229         then the same applies.
230         """
231         self.__setTopLevelDir()
232         if name[0] == '#':
233             directory = self.Top
234             name = os.path.join(os.path.normpath('./'), name[1:])
235         elif not directory:
236             directory = self._cwd
237         return (name, directory)
238
239     def chdir(self, dir):
240         """Change the current working directory for lookups.
241         """
242         self.__setTopLevelDir()
243         if not dir is None:
244             self._cwd = dir
245
246     def Entry(self, name, directory = None):
247         """Lookup or create a generic Entry node with the specified name.
248         If the name is a relative path (begins with ./, ../, or a file
249         name), then it is looked up relative to the supplied directory
250         node, or to the top level directory of the FS (supplied at
251         construction time) if no directory is supplied.
252         """
253         name, directory = self.__transformPath(name, directory)
254         return self.__doLookup(Entry, name, directory)
255     
256     def File(self, name, directory = None):
257         """Lookup or create a File node with the specified name.  If
258         the name is a relative path (begins with ./, ../, or a file name),
259         then it is looked up relative to the supplied directory node,
260         or to the top level directory of the FS (supplied at construction
261         time) if no directory is supplied.
262
263         This method will raise TypeError if a directory is found at the
264         specified path.
265         """
266         name, directory = self.__transformPath(name, directory)
267         return self.__doLookup(File, name, directory)
268
269     def Dir(self, name, directory = None):
270         """Lookup or create a Dir node with the specified name.  If
271         the name is a relative path (begins with ./, ../, or a file name),
272         then it is looked up relative to the supplied directory node,
273         or to the top level directory of the FS (supplied at construction
274         time) if no directory is supplied.
275
276         This method will raise TypeError if a normal file is found at the
277         specified path.
278         """
279         name, directory = self.__transformPath(name, directory)
280         return self.__doLookup(Dir, name, directory)
281
282     def BuildDir(self, build_dir, src_dir):
283         """Link the supplied build directory to the source directory
284         for purposes of building files."""
285         self.__setTopLevelDir()
286         dirSrc = self.Dir(src_dir)
287         dirBuild = self.Dir(build_dir)
288         if not dirSrc.is_under(self.Top) or not dirBuild.is_under(self.Top):
289             raise UserError, "Both source and build directories must be under top of build tree."
290         if dirSrc.is_under(dirBuild):
291             raise UserError, "Source directory cannot be under build directory."
292         dirBuild.link(dirSrc)
293         
294
295 class Entry(SCons.Node.Node):
296     """A generic class for file system entries.  This class if for
297     when we don't know yet whether the entry being looked up is a file
298     or a directory.  Instances of this class can morph into either
299     Dir or File objects by a later, more precise lookup."""
300
301     def __init__(self, name, directory):
302         """Initialize a generic file system Entry.
303         
304         Call the superclass initialization, take care of setting up
305         our relative and absolute paths, identify our parent
306         directory, and indicate that this node should use
307         signatures."""
308         SCons.Node.Node.__init__(self)
309
310         self.name = name
311         if directory:
312             self.abspath = os.path.join(directory.abspath, name)
313             if str(directory.path) == '.':
314                 self.path = name
315             else:
316                 self.path = os.path.join(directory.path, name)
317         else:
318             self.abspath = self.path = name
319         self.path_ = self.path
320         self.abspath_ = self.abspath
321         self.dir = directory
322         self.use_signature = 1
323         self.__doSrcpath()
324
325     def adjust_srcpath(self):
326         self.__doSrcpath()
327         
328     def __doSrcpath(self):
329         if self.dir:
330             if str(self.dir.srcpath) == '.':
331                 self.srcpath = self.name
332             else:
333                 self.srcpath = os.path.join(self.dir.srcpath, self.name)
334         else:
335             self.srcpath = self.name
336
337     def __str__(self):
338         """A FS node's string representation is its path name."""
339         return self.path
340
341     def __cmp__(self, other):
342         if type(self) != types.StringType and type(other) != types.StringType:
343             try:
344                 if self.__class__ != other.__class__:
345                     return 1
346             except:
347                 return 1
348         return cmp(str(self), str(other))
349
350     def __hash__(self):
351         return hash(self.abspath_)
352
353     def exists(self):
354         return os.path.exists(self.abspath)
355
356     def current(self):
357         """If the underlying path doesn't exist, we know the node is
358         not current without even checking the signature, so return 0.
359         Otherwise, return None to indicate that signature calculation
360         should proceed as normal to find out if the node is current."""
361         if not self.exists():
362             return 0
363         return None
364
365     def is_under(self, dir):
366         if self is dir:
367             return 1
368         if not self.dir:
369             return 0
370         return self.dir.is_under(dir)
371
372
373
374 # XXX TODO?
375 # Annotate with the creator
376 # is_under
377 # rel_path
378 # srcpath / srcdir
379 # link / is_linked
380 # linked_targets
381 # is_accessible
382
383 class Dir(Entry):
384     """A class for directories in a file system.
385     """
386
387     def __init__(self, name, directory = None):
388         Entry.__init__(self, name, directory)
389         self._morph()
390
391     def _morph(self):
392         """Turn a file system node (either a freshly initialized
393         directory object or a separate Entry object) into a
394         proper directory object.
395         
396         Modify our paths to add the trailing slash that indicates
397         a directory.  Set up this directory's entries and hook it
398         into the file system tree.  Specify that directories (this
399         node) don't use signatures for currency calculation."""
400
401         self.path_ = os.path.join(self.path, '')
402         self.abspath_ = os.path.join(self.abspath, '')
403
404         self.entries = PathDict()
405         self.entries['.'] = self
406         if hasattr(self, 'dir'):
407             self.entries['..'] = self.dir
408         else:
409             self.entries['..'] = None
410         self.use_signature = None
411         self.builder = 1
412         self._sconsign = None
413
414     def __doReparent(self):
415         for ent in self.entries.values():
416             if not ent is self and not ent is self.dir:
417                 ent.adjust_srcpath()
418
419     def adjust_srcpath(self):
420         Entry.adjust_srcpath(self)
421         self.__doReparent()
422                 
423     def link(self, srcdir):
424         """Set this directory as the build directory for the
425         supplied source directory."""
426         self.srcpath = srcdir.path
427         self.__doReparent()
428
429     def up(self):
430         return self.entries['..']
431
432     def root(self):
433         if not self.entries['..']:
434             return self
435         else:
436             return self.entries['..'].root()
437
438     def children(self):
439         #XXX --random:  randomize "dependencies?"
440         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
441         kids = map(lambda x, s=self: s.entries[x], keys)
442         def c(one, two):
443             if one.abspath < two.abspath:
444                return -1
445             if one.abspath > two.abspath:
446                return 1
447             return 0
448         kids.sort(c)
449         return kids
450
451     def build(self):
452         """A null "builder" for directories."""
453         pass
454
455     def set_bsig(self, bsig):
456         """A directory has no signature."""
457         pass
458
459     def set_csig(self, csig):
460         """A directory has no signature."""
461         pass
462
463     def current(self):
464         """If all of our children were up-to-date, then this
465         directory was up-to-date, too."""
466         state = 0
467         for kid in self.children():
468             s = kid.get_state()
469             if s and (not state or s > state):
470                 state = s
471         import SCons.Node
472         if state == SCons.Node.up_to_date:
473             return 1
474         else:
475             return 0
476
477     def sconsign(self):
478         """Return the .sconsign file info for this directory,
479         creating it first if necessary."""
480         if not self._sconsign:
481             #XXX Rework this to get rid of the hard-coding
482             import SCons.Sig
483             import SCons.Sig.MD5
484             self._sconsign = SCons.Sig.SConsignFile(self, SCons.Sig.MD5)
485         return self._sconsign
486
487
488 # XXX TODO?
489 # rfile
490 # precious
491 # no_rfile
492 # rpath
493 # rsrcpath
494 # source_exists
495 # derived_exists
496 # is_on_rpath
497 # local
498 # base_suf
499 # suffix
500 # addsuffix
501 # accessible
502 # ignore
503 # build
504 # bind
505 # is_under
506 # relpath
507
508 class File(Entry):
509     """A class for files in a file system.
510     """
511     def __init__(self, name, directory = None):
512         Entry.__init__(self, name, directory)
513         self._morph()
514         
515     def _morph(self):
516         """Turn a file system node into a File object."""
517         self.created = 0
518
519     def root(self):
520         return self.dir.root()
521
522     def get_contents(self):
523         if not self.exists():
524             return ''
525         return open(str(self), "r").read()
526
527     def get_timestamp(self):
528         if self.exists():
529             return os.path.getmtime(self.path)
530         else:
531             return 0
532
533     def set_bsig(self, bsig):
534         """Set the build signature for this file, updating the
535         .sconsign entry."""
536         Entry.set_bsig(self, bsig)
537         self.set_sconsign()
538
539     def set_csig(self, csig):
540         """Set the content signature for this file, updating the
541         .sconsign entry."""
542         Entry.set_csig(self, csig)
543         self.set_sconsign()
544
545     def set_sconsign(self):
546         """Update a file's .sconsign entry with its current info."""
547         self.dir.sconsign().set(self.name, self.get_timestamp(),
548                                 self.get_bsig(), self.get_csig())
549
550     def get_prevsiginfo(self):
551         """Fetch the previous signature information from the
552         .sconsign entry."""
553         return self.dir.sconsign().get(self.name)
554
555     def scan(self):
556         if self.env:
557             for scn in self.scanners:
558                 if not self.scanned.has_key(scn):
559                     self.add_implicit(scn.scan(self.path, self.env),
560                                       scn)
561                     self.scanned[scn] = 1
562                     
563     def exists(self):
564         if not self.created:
565             self.created = 1
566             if self.srcpath != self.path and \
567                os.path.exists(self.srcpath):
568                 if os.path.exists(self.path):
569                     os.unlink(self.path)
570                 self.__createDir()
571                 file_link(self.srcpath, self.path)
572         return Entry.exists(self)
573
574     def __createDir(self):
575         # ensure that the directories for this node are
576         # created.
577
578         listPaths = []
579         strPath = self.abspath
580         while 1:
581             strPath, strFile = os.path.split(strPath)
582             if os.path.exists(strPath):
583                 break
584             listPaths.append(strPath)
585             if not strFile:
586                 break
587         listPaths.reverse()
588         for strPath in listPaths:
589             try:
590                 os.mkdir(strPath)
591             except OSError:
592                 pass
593
594     def build(self):
595         self.__createDir()
596         Entry.build(self)
597
598 default_fs = FS()