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
12 # Copyright (c) 2001 Steven Knight
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:
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
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.
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
40 from UserDict import UserDict
42 from SCons.Errors import UserError
47 except AttributeError:
50 def file_link(src, dest):
51 shutil.copyfile(src, dest)
53 os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
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."""
64 convert_path = unicode
68 def __init__(self, path_name=''):
69 self.data = PathName.convert_path(path_name)
70 self.norm_path = os.path.normcase(self.data)
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)),
83 return repr(self.data)
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
93 All keys are implicitly converted to PathName objects before
94 insertion into the dictionary."""
96 def __init__(self, initdict = {}):
97 UserDict.__init__(self, initdict)
100 for key, val in old_dict.items():
101 self.data[PathName(key)] = val
103 def __setitem__(self, key, val):
104 self.data[PathName(key)] = val
106 def __getitem__(self, key):
107 return self.data[PathName(key)]
109 def __delitem__(self, key):
110 del(self.data[PathName(key)])
112 def setdefault(self, key, value):
114 return self.data[PathName(key)]
116 self.data[PathName(key)] = value
120 def __init__(self, path = None):
121 """Initialize the Node.FS subsystem.
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.
127 The path argument must be a valid absolute path.
130 self.pathTop = os.getcwd()
133 self.Root = PathDict()
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."
140 def __setTopLevelDir(self):
142 self.Top = self.__doLookup(Dir, self.pathTop)
144 self.Top.srcpath = '.'
145 self.Top.path_ = os.path.join('.', '')
149 self.__setTopLevelDir()
150 return hash(self.Top)
152 def __cmp__(self, other):
153 self.__setTopLevelDir()
154 if isinstance(other, FS):
155 other.__setTopLevelDir()
156 return cmp(self.__dict__, other.__dict__)
159 self.__setTopLevelDir()
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
170 head, tail = os.path.split(os.path.normpath(name))
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.
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.
185 drive, tail = os.path.splitdrive(head)
186 #if sys.platform is 'win32' and not drive:
188 # raise OSError, 'No drive letter supplied for absolute path.'
189 # return directory.root()
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)
198 # Recursively look up our parent directories.
199 directory = self.__doLookup(Dir, head, directory)
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.
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
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__)
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().
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.
228 If directory is None, and name is a relative path,
229 then the same applies.
231 self.__setTopLevelDir()
234 name = os.path.join(os.path.normpath('./'), name[1:])
236 directory = self._cwd
237 return (name, directory)
239 def chdir(self, dir):
240 """Change the current working directory for lookups.
242 self.__setTopLevelDir()
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.
253 name, directory = self.__transformPath(name, directory)
254 return self.__doLookup(Entry, name, directory)
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.
263 This method will raise TypeError if a directory is found at the
266 name, directory = self.__transformPath(name, directory)
267 return self.__doLookup(File, name, directory)
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.
276 This method will raise TypeError if a normal file is found at the
279 name, directory = self.__transformPath(name, directory)
280 return self.__doLookup(Dir, name, directory)
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)
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."""
301 def __init__(self, name, directory):
302 """Initialize a generic file system Entry.
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
308 SCons.Node.Node.__init__(self)
312 self.abspath = os.path.join(directory.abspath, name)
313 if str(directory.path) == '.':
316 self.path = os.path.join(directory.path, name)
318 self.abspath = self.path = name
319 self.path_ = self.path
320 self.abspath_ = self.abspath
322 self.use_signature = 1
325 def adjust_srcpath(self):
328 def __doSrcpath(self):
330 if str(self.dir.srcpath) == '.':
331 self.srcpath = self.name
333 self.srcpath = os.path.join(self.dir.srcpath, self.name)
335 self.srcpath = self.name
338 """A FS node's string representation is its path name."""
341 def __cmp__(self, other):
342 if type(self) != types.StringType and type(other) != types.StringType:
344 if self.__class__ != other.__class__:
348 return cmp(str(self), str(other))
351 return hash(self.abspath_)
354 return os.path.exists(self.abspath)
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():
365 def is_under(self, dir):
370 return self.dir.is_under(dir)
375 # Annotate with the creator
384 """A class for directories in a file system.
387 def __init__(self, name, directory = None):
388 Entry.__init__(self, name, directory)
392 """Turn a file system node (either a freshly initialized
393 directory object or a separate Entry object) into a
394 proper directory object.
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."""
401 self.path_ = os.path.join(self.path, '')
402 self.abspath_ = os.path.join(self.abspath, '')
404 self.entries = PathDict()
405 self.entries['.'] = self
406 if hasattr(self, 'dir'):
407 self.entries['..'] = self.dir
409 self.entries['..'] = None
410 self.use_signature = None
412 self._sconsign = None
414 def __doReparent(self):
415 for ent in self.entries.values():
416 if not ent is self and not ent is self.dir:
419 def adjust_srcpath(self):
420 Entry.adjust_srcpath(self)
423 def link(self, srcdir):
424 """Set this directory as the build directory for the
425 supplied source directory."""
426 self.srcpath = srcdir.path
430 return self.entries['..']
433 if not self.entries['..']:
436 return self.entries['..'].root()
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)
443 if one.abspath < two.abspath:
445 if one.abspath > two.abspath:
452 """A null "builder" for directories."""
455 def set_bsig(self, bsig):
456 """A directory has no signature."""
459 def set_csig(self, csig):
460 """A directory has no signature."""
464 """If all of our children were up-to-date, then this
465 directory was up-to-date, too."""
467 for kid in self.children():
469 if s and (not state or s > state):
472 if state == SCons.Node.up_to_date:
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
484 self._sconsign = SCons.Sig.SConsignFile(self, SCons.Sig.MD5)
485 return self._sconsign
509 """A class for files in a file system.
511 def __init__(self, name, directory = None):
512 Entry.__init__(self, name, directory)
516 """Turn a file system node into a File object."""
520 return self.dir.root()
522 def get_contents(self):
523 if not self.exists():
525 return open(str(self), "r").read()
527 def get_timestamp(self):
529 return os.path.getmtime(self.path)
533 def set_bsig(self, bsig):
534 """Set the build signature for this file, updating the
536 Entry.set_bsig(self, bsig)
539 def set_csig(self, csig):
540 """Set the content signature for this file, updating the
542 Entry.set_csig(self, csig)
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())
550 def get_prevsiginfo(self):
551 """Fetch the previous signature information from the
553 return self.dir.sconsign().get(self.name)
557 for scn in self.scanners:
558 if not self.scanned.has_key(scn):
559 self.add_implicit(scn.scan(self.path, self.env),
561 self.scanned[scn] = 1
566 if self.srcpath != self.path and \
567 os.path.exists(self.srcpath):
568 if os.path.exists(self.path):
571 file_link(self.srcpath, self.path)
572 return Entry.exists(self)
574 def __createDir(self):
575 # ensure that the directories for this node are
579 strPath = self.abspath
581 strPath, strFile = os.path.split(strPath)
582 if os.path.exists(strPath):
584 listPaths.append(strPath)
588 for strPath in listPaths: