# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
+from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+from itertools import izip
+import cStringIO
import fnmatch
import os
import os.path
import re
import shutil
import stat
-import string
import sys
import time
-import cStringIO
+
+try:
+ import codecs
+except ImportError:
+ pass
+else:
+ # TODO(2.2): Remove when 2.3 becomes the minimal supported version.
+ try:
+ codecs.BOM_UTF8
+ except AttributeError:
+ codecs.BOM_UTF8 = '\xef\xbb\xbf'
+ try:
+ codecs.BOM_UTF16_LE
+ codecs.BOM_UTF16_BE
+ except AttributeError:
+ codecs.BOM_UTF16_LE = '\xff\xfe'
+ codecs.BOM_UTF16_BE = '\xfe\xff'
+
+ # Provide a wrapper function to handle decoding differences in
+ # different versions of Python. Normally, we'd try to do this in the
+ # compat layer (and maybe it still makes sense to move there?) but
+ # that doesn't provide a way to supply the string class used in
+ # pre-2.3 Python versions with a .decode() method that all strings
+ # naturally have. Plus, the 2.[01] encodings behave differently
+ # enough that we have to settle for a lowest-common-denominator
+ # wrapper approach.
+ #
+ # Note that the 2.[012] implementations below may be inefficient
+ # because they perform an explicit look up of the encoding for every
+ # decode, but they're old enough (and we want to stop supporting
+ # them soon enough) that it's not worth complicating the interface.
+ # Think of it as additional incentive for people to upgrade...
+ try:
+ ''.decode
+ except AttributeError:
+ # 2.0 through 2.2: strings have no .decode() method
+ try:
+ codecs.lookup('ascii').decode
+ except AttributeError:
+ # 2.0 and 2.1: encodings are a tuple of functions, and the
+ # decode() function returns a (result, length) tuple.
+ def my_decode(contents, encoding):
+ return codecs.lookup(encoding)[1](contents)[0]
+ else:
+ # 2.2: encodings are an object with methods, and the
+ # .decode() method returns just the decoded bytes.
+ def my_decode(contents, encoding):
+ return codecs.lookup(encoding).decode(contents)
+ else:
+ # 2.3 or later: use the .decode() string method
+ def my_decode(contents, encoding):
+ return contents.decode(encoding)
import SCons.Action
from SCons.Debug import logInstanceCreation
from SCons.Debug import Trace
+do_store_info = True
+
+
+class EntryProxyAttributeError(AttributeError):
+ """
+ An AttributeError subclass for recording and displaying the name
+ of the underlying Entry involved in an AttributeError exception.
+ """
+ def __init__(self, entry_proxy, attribute):
+ AttributeError.__init__(self)
+ self.entry_proxy = entry_proxy
+ self.attribute = attribute
+ def __str__(self):
+ entry = self.entry_proxy.get()
+ fmt = "%s instance %s has no attribute %s"
+ return fmt % (entry.__class__.__name__,
+ repr(entry.name),
+ repr(self.attribute))
+
# The max_drift value: by default, use a cached signature value for
# any file that's been untouched for more than two days.
default_max_drift = 2*24*60*60
#
# A number of the above factors, however, can be set after we've already
# been asked to return a string for a Node, because a Repository() or
-# BuildDir() call or the like may not occur until later in SConscript
+# VariantDir() call or the like may not occur until later in SConscript
# files. So this variable controls whether we bother trying to save
# string values for Nodes. The wrapper interface can set this whenever
-# they're done mucking with Repository and BuildDir and the other stuff,
+# they're done mucking with Repository and VariantDir and the other stuff,
# to let this module know it can start returning saved string values
# for Nodes.
#
"should be in Valid_Duplicates")
global Link_Funcs
Link_Funcs = []
- for func in string.split(duplicate,'-'):
+ for func in duplicate.split('-'):
if link_dict[func]:
Link_Funcs.append(link_dict[func])
if func == Link_Funcs[-1]:
# exception of the last link method (copy) are fatal
raise
- else:
- pass
return 0
Link = SCons.Action.Action(LinkFunc, None)
return x
else:
def _my_normcase(x):
- return string.upper(x)
+ return x.upper()
dc.set(list)
def diskcheck_types():
- return map(lambda dc: dc.type, diskcheckers)
+ return [dc.type for dc in diskcheckers]
return self
else:
entry = self.get()
- r = string.replace(entry.get_path(), os.sep, '/')
+ r = entry.get_path().replace(os.sep, '/')
return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
def __get_windows_path(self):
return self
else:
entry = self.get()
- r = string.replace(entry.get_path(), os.sep, '\\')
+ r = entry.get_path().replace(os.sep, '\\')
return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
def __get_srcnode(self):
def __get_srcdir(self):
"""Returns the directory containing the source node linked to this
- node via BuildDir(), or the directory of this node if not linked."""
+ node via VariantDir(), or the directory of this node if not linked."""
return EntryProxy(self.get().srcnode().dir)
def __get_rsrcnode(self):
def __get_rsrcdir(self):
"""Returns the directory containing the source node linked to this
- node via BuildDir(), or the directory of this node if not linked."""
+ node via VariantDir(), or the directory of this node if not linked."""
return EntryProxy(self.get().srcnode().rfile().dir)
def __get_dir(self):
except KeyError:
try:
attr = SCons.Util.Proxy.__getattr__(self, name)
- except AttributeError:
- entry = self.get()
- classname = string.split(str(entry.__class__), '.')[-1]
- if classname[-2:] == "'>":
- # new-style classes report their name as:
- # "<class 'something'>"
- # instead of the classic classes:
- # "something"
- classname = classname[:-2]
- raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
+ except AttributeError, e:
+ # Raise our own AttributeError subclass with an
+ # overridden __str__() method that identifies the
+ # name of the entry that caused the exception.
+ raise EntryProxyAttributeError(self, name)
return attr
else:
return attr_function(self)
if __debug__: logInstanceCreation(self, 'Node.FS.Base')
SCons.Node.Node.__init__(self)
- self.name = name
- self.suffix = SCons.Util.splitext(name)[1]
+ # Filenames and paths are probably reused and are intern'ed to
+ # save some memory.
+ self.name = SCons.Util.silent_intern(name)
+ self.suffix = SCons.Util.silent_intern(SCons.Util.splitext(name)[1])
self.fs = fs
assert directory, "A directory must be provided"
- self.abspath = directory.entry_abspath(name)
- self.labspath = directory.entry_labspath(name)
+ self.abspath = SCons.Util.silent_intern(directory.entry_abspath(name))
+ self.labspath = SCons.Util.silent_intern(directory.entry_labspath(name))
if directory.path == '.':
- self.path = name
+ self.path = SCons.Util.silent_intern(name)
else:
- self.path = directory.entry_path(name)
+ self.path = SCons.Util.silent_intern(directory.entry_path(name))
if directory.tpath == '.':
- self.tpath = name
+ self.tpath = SCons.Util.silent_intern(name)
else:
- self.tpath = directory.entry_tpath(name)
+ self.tpath = SCons.Util.silent_intern(directory.entry_tpath(name))
self.path_elements = directory.path_elements + [self]
self.dir = directory
self.cwd = None # will hold the SConscript directory for target nodes
self.duplicate = directory.duplicate
+ def str_for_display(self):
+ return '"' + self.__str__() + '"'
+
def must_be_same(self, klass):
"""
This node, which already existed, is being looked up as the
specified klass. Raise an exception if it isn't.
"""
- if self.__class__ is klass or klass is Entry:
+ if isinstance(self, klass) or klass is Entry:
return
raise TypeError, "Tried to lookup %s '%s' as a %s." %\
(self.__class__.__name__, self.path, klass.__name__)
return self._memo['_save_str']
except KeyError:
pass
- result = self._get_str()
+ result = intern(self._get_str())
self._memo['_save_str'] = result
return result
if self.duplicate or self.is_derived():
return self.get_path()
srcnode = self.srcnode()
- if srcnode.stat() is None and not self.stat() is None:
+ if srcnode.stat() is None and self.stat() is not None:
result = self.get_path()
else:
result = srcnode.get_path()
# values that the underlying stat() method saved.
try: del self._memo['stat']
except KeyError: pass
- if not self is srcnode:
+ if self is not srcnode:
try: del srcnode._memo['stat']
except KeyError: pass
return result
return result
def exists(self):
- return not self.stat() is None
+ return self.stat() is not None
def rexists(self):
return self.rfile().exists()
def isdir(self):
st = self.stat()
- return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
+ return st is not None and stat.S_ISDIR(st[stat.ST_MODE])
def isfile(self):
st = self.stat()
- return not st is None and stat.S_ISREG(st[stat.ST_MODE])
+ return st is not None and stat.S_ISREG(st[stat.ST_MODE])
if hasattr(os, 'symlink'):
def islink(self):
try: i = path_elems.index(dir)
except ValueError: pass
else: path_elems = path_elems[i+1:]
- path_elems = map(lambda n: n.name, path_elems)
- return string.join(path_elems, os.sep)
+ path_elems = [n.name for n in path_elems]
+ return os.sep.join(path_elems)
def set_src_builder(self, builder):
"""Set the source code builder for this node."""
return self.get_suffix()
def get_contents(self):
- """Fetch the contents of the entry.
-
- Since this should return the real contents from the file
- system, we check to see into what sort of subclass we should
- morph this Entry."""
+ """Fetch the contents of the entry. Returns the exact binary
+ contents of the file."""
try:
self = self.disambiguate(must_exist=1)
except SCons.Errors.UserError:
else:
return self.get_contents()
+ def get_text_contents(self):
+ """Fetch the decoded text contents of a Unicode encoded Entry.
+
+ Since this should return the text contents from the file
+ system, we check to see into what sort of subclass we should
+ morph this Entry."""
+ try:
+ self = self.disambiguate(must_exist=1)
+ except SCons.Errors.UserError:
+ # There was nothing on disk with which to disambiguate
+ # this entry. Leave it as an Entry, but return a null
+ # string so calls to get_text_contents() in emitters and
+ # the like (e.g. in qt.py) don't have to disambiguate by
+ # hand or catch the exception.
+ return ''
+ else:
+ return self.get_text_contents()
+
def must_be_same(self, klass):
"""Called to make sure a Node is a Dir. Since we're an
Entry, we can morph into one."""
- if not self.__class__ is klass:
+ if self.__class__ is not klass:
self.__class__ = klass
self._morph()
- self.clear
+ self.clear()
# The following methods can get called before the Taskmaster has
# had a chance to call disambiguate() directly to see if this Entry
def rel_path(self, other):
d = self.disambiguate()
- if d.__class__ == Entry:
+ if d.__class__ is Entry:
raise "rel_path() could not disambiguate File/Dir"
return d.rel_path(other)
def _glob1(self, pattern, ondisk=True, source=False, strings=False):
return self.disambiguate()._glob1(pattern, ondisk, source, strings)
+ def get_subst_proxy(self):
+ return self.disambiguate().get_subst_proxy()
+
# This is for later so we can differentiate between Entry the class and Entry
# the method of the FS class.
_classEntry = Entry
"""
curr=self._cwd
try:
- if not dir is None:
+ if dir is not None:
self._cwd = dir
if change_os_dir:
os.chdir(dir.abspath)
root = directory.root
if os.sep != '/':
- p = string.replace(p, os.sep, '/')
+ p = p.replace(os.sep, '/')
return root._lookup_abs(p, fsclass, create)
def Entry(self, name, directory = None, create = 1):
- """Lookup or create a generic Entry node with the specified name.
+ """Look up or create a generic Entry node with the specified name.
If the name is a relative path (begins with ./, ../, or a file
name), then it is looked up relative to the supplied directory
node, or to the top level directory of the FS (supplied at
return self._lookup(name, directory, Entry, create)
def File(self, name, directory = None, create = 1):
- """Lookup or create a File node with the specified name. If
+ """Look up or create a File node with the specified name. If
the name is a relative path (begins with ./, ../, or a file name),
then it is looked up relative to the supplied directory node,
or to the top level directory of the FS (supplied at construction
return self._lookup(name, directory, File, create)
def Dir(self, name, directory = None, create = True):
- """Lookup or create a Dir node with the specified name. If
+ """Look up or create a Dir node with the specified name. If
the name is a relative path (begins with ./, ../, or a file name),
then it is looked up relative to the supplied directory node,
or to the top level directory of the FS (supplied at construction
"""
return self._lookup(name, directory, Dir, create)
- def BuildDir(self, build_dir, src_dir, duplicate=1):
- """Link the supplied build directory to the source directory
+ def VariantDir(self, variant_dir, src_dir, duplicate=1):
+ """Link the supplied variant directory to the source directory
for purposes of building files."""
if not isinstance(src_dir, SCons.Node.Node):
src_dir = self.Dir(src_dir)
- if not isinstance(build_dir, SCons.Node.Node):
- build_dir = self.Dir(build_dir)
- if src_dir.is_under(build_dir):
- raise SCons.Errors.UserError, "Source directory cannot be under build directory."
- if build_dir.srcdir:
- if build_dir.srcdir == src_dir:
+ if not isinstance(variant_dir, SCons.Node.Node):
+ variant_dir = self.Dir(variant_dir)
+ if src_dir.is_under(variant_dir):
+ raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
+ if variant_dir.srcdir:
+ if variant_dir.srcdir == src_dir:
return # We already did this.
- raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
- build_dir.link(src_dir, duplicate)
+ raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
+ variant_dir.link(src_dir, duplicate)
def Repository(self, *dirs):
"""Specify Repository directories to search."""
d = self.Dir(d)
self.Top.addRepository(d)
- def build_dir_target_climb(self, orig, dir, tail):
- """Create targets in corresponding build directories
+ def variant_dir_target_climb(self, orig, dir, tail):
+ """Create targets in corresponding variant directories
Climb the directory tree, and look up path names
- relative to any linked build directories we find.
+ relative to any linked variant directories we find.
Even though this loops and walks up the tree, we don't memoize
the return value because this is really only used to process
"""
targets = []
message = None
- fmt = "building associated BuildDir targets: %s"
+ fmt = "building associated VariantDir targets: %s"
start_dir = dir
while dir:
- for bd in dir.build_dirs:
+ for bd in dir.variant_dirs:
if start_dir.is_under(bd):
# If already in the build-dir location, don't reflect
return [orig], fmt % str(orig)
- p = apply(os.path.join, [bd.path] + tail)
+ p = os.path.join(bd.path, *tail)
targets.append(self.Entry(p))
tail = [dir.name] + tail
dir = dir.up()
if targets:
- message = fmt % string.join(map(str, targets))
+ message = fmt % ' '.join(map(str, targets))
return targets, message
def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
self.cwd = self
self.searched = 0
self._sconsign = None
- self.build_dirs = []
+ self.variant_dirs = []
self.root = self.dir.root
# Don't just reset the executor, replace its action list,
del node._srcreps
except AttributeError:
pass
- if duplicate != None:
+ if duplicate is not None:
node.duplicate=duplicate
def __resetDuplicate(self, node):
Looks up or creates a directory node named 'name' relative to
this directory.
"""
- dir = self.fs.Dir(name, self, create)
- return dir
+ return self.fs.Dir(name, self, create)
def File(self, name):
"""
a path containing '..'), an absolute path name, a top-relative
('#foo') path name, or any kind of object.
"""
- name = self.labspath + '/' + name
+ name = self.entry_labspath(name)
return self.root._lookup_abs(name, klass, create)
def link(self, srcdir, duplicate):
- """Set this directory as the build directory for the
+ """Set this directory as the variant directory for the
supplied source directory."""
self.srcdir = srcdir
self.duplicate = duplicate
self.__clearRepositoryCache(duplicate)
- srcdir.build_dirs.append(self)
+ srcdir.variant_dirs.append(self)
def getRepositories(self):
"""Returns a list of repositories for this directory.
def get_all_rdirs(self):
try:
- return self._memo['get_all_rdirs']
+ return list(self._memo['get_all_rdirs'])
except KeyError:
pass
fname = dir.name + os.sep + fname
dir = dir.up()
- self._memo['get_all_rdirs'] = result
+ self._memo['get_all_rdirs'] = list(result)
return result
"""Return a path to "other" relative to this directory.
"""
- # This complicated and expensive method, which constructs relative
- # paths between arbitrary Node.FS objects, is no longer used
- # by SCons itself. It was introduced to store dependency paths
- # in .sconsign files relative to the target, but that ended up
- # being significantly inefficient.
+ # This complicated and expensive method, which constructs relative
+ # paths between arbitrary Node.FS objects, is no longer used
+ # by SCons itself. It was introduced to store dependency paths
+ # in .sconsign files relative to the target, but that ended up
+ # being significantly inefficient.
#
- # We're continuing to support the method because some SConstruct
- # files out there started using it when it was available, and
- # we're all about backwards compatibility..
+ # We're continuing to support the method because some SConstruct
+ # files out there started using it when it was available, and
+ # we're all about backwards compatibility..
try:
memo_dict = self._memo['rel_path']
pass
if self is other:
-
result = '.'
elif not other in self.path_elements:
-
try:
other_dir = other.get_dir()
except AttributeError:
result = other.name
else:
result = dir_rel_path + os.sep + other.name
-
else:
-
i = self.path_elements.index(other) + 1
path_elems = ['..'] * (len(self.path_elements) - i) \
- + map(lambda n: n.name, other.path_elements[i:])
+ + [n.name for n in other.path_elements[i:]]
- result = string.join(path_elems, os.sep)
+ result = os.sep.join(path_elems)
memo_dict[other] = result
def build(self, **kw):
"""A null "builder" for directories."""
global MkdirBuilder
- if not self.builder is MkdirBuilder:
- apply(SCons.Node.Node.build, [self,], kw)
+ if self.builder is not MkdirBuilder:
+ SCons.Node.Node.build(self, **kw)
#
#
listDirs.append(parent)
p = parent.up()
if p is None:
+ # Don't use while: - else: for this condition because
+ # if so, then parent is None and has no .path attribute.
raise SCons.Errors.StopError, parent.path
parent = p
listDirs.reverse()
def multiple_side_effect_has_builder(self):
global MkdirBuilder
- return not self.builder is MkdirBuilder and self.has_builder()
+ return self.builder is not MkdirBuilder and self.has_builder()
def alter_targets(self):
- """Return any corresponding targets in a build directory.
+ """Return any corresponding targets in a variant directory.
"""
- return self.fs.build_dir_target_climb(self, self, [])
+ return self.fs.variant_dir_target_climb(self, self, [])
def scanner_key(self):
"""A directory does not get scanned."""
return None
+ def get_text_contents(self):
+ """We already emit things in text, so just return the binary
+ version."""
+ return self.get_contents()
+
def get_contents(self):
- """Return aggregate contents of all our children."""
- contents = map(lambda n: n.get_contents(), self.children())
- return string.join(contents, '')
+ """Return content signatures and names of all our children
+ separated by new-lines. Ensure that the nodes are sorted."""
+ contents = []
+ for node in sorted(self.children(), key=lambda t: t.name):
+ contents.append('%s %s\n' % (node.get_csig(), node.name))
+ return ''.join(contents)
+
+ def get_csig(self):
+ """Compute the content signature for Directory nodes. In
+ general, this is not needed and the content signature is not
+ stored in the DirNodeInfo. However, if get_contents on a Dir
+ node is called which has a child directory, the child
+ directory should return the hash of its contents."""
+ contents = self.get_contents()
+ return SCons.Util.MD5signature(contents)
def do_duplicate(self, src):
pass
def is_up_to_date(self):
"""If any child is not up-to-date, then this directory isn't,
either."""
- if not self.builder is MkdirBuilder and not self.exists():
+ if self.builder is not MkdirBuilder and not self.exists():
return 0
up_to_date = SCons.Node.up_to_date
for kid in self.children():
pass
else:
for entry in map(_my_normcase, entries):
- d[entry] = 1
+ d[entry] = True
self.on_disk_entries = d
- return d.has_key(_my_normcase(name))
+ if sys.platform == 'win32':
+ name = _my_normcase(name)
+ result = d.get(name)
+ if result is None:
+ # Belt-and-suspenders for Windows: check directly for
+ # 8.3 file names that don't show up in os.listdir().
+ result = os.path.exists(self.abspath + os.sep + name)
+ d[name] = result
+ return result
+ else:
+ return name in d
memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
for dir in self.srcdir_list():
if self.is_under(dir):
# We shouldn't source from something in the build path;
- # build_dir is probably under src_dir, in which case
+ # variant_dir is probably under src_dir, in which case
# we are reflecting.
break
if dir.entry_exists_on_disk(name):
if self.entry_exists_on_disk(name):
try: return self.Dir(name)
except TypeError: pass
- return None
+ node = self.srcdir_duplicate(name)
+ if isinstance(node, File):
+ return None
+ return node
def file_on_disk(self, name):
if self.entry_exists_on_disk(name) or \
except TypeError: pass
node = self.srcdir_duplicate(name)
if isinstance(node, Dir):
- node = None
+ return None
return node
def walk(self, func, arg):
names.remove('.')
names.remove('..')
func(arg, self, names)
- select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
- for dirname in filter(select_dirs, names):
+ for dirname in [n for n in names if isinstance(entries[n], Dir)]:
entries[dirname].walk(func, arg)
def glob(self, pathname, ondisk=True, source=False, strings=False):
The "source" argument, when true, specifies that corresponding
source Nodes must be returned if you're globbing in a build
- directory (initialized with BuildDir()). The default behavior
- is to return Nodes local to the BuildDir().
+ directory (initialized with VariantDir()). The default behavior
+ is to return Nodes local to the VariantDir().
The "strings" argument, when true, returns the matches as strings,
not Nodes. The strings are path names relative to this directory.
"""
dirname, basename = os.path.split(pathname)
if not dirname:
- return self._glob1(basename, ondisk, source, strings)
+ return sorted(self._glob1(basename, ondisk, source, strings),
+ key=lambda t: str(t))
if has_glob_magic(dirname):
list = self.glob(dirname, ondisk, source, strings=False)
else:
for dir in list:
r = dir._glob1(basename, ondisk, source, strings)
if strings:
- r = map(lambda x, d=str(dir): os.path.join(d, x), r)
+ r = [os.path.join(str(dir), x) for x in r]
result.extend(r)
+ result.sort(lambda a, b: cmp(str(a), str(b)))
return result
def _glob1(self, pattern, ondisk=True, source=False, strings=False):
for srcdir in self.srcdir_list():
search_dir_list.extend(srcdir.get_all_rdirs())
+ selfEntry = self.Entry
names = []
for dir in search_dir_list:
# We use the .name attribute from the Node because the keys of
# the dir.entries dictionary are normalized (that is, all upper
# case) on case-insensitive systems like Windows.
- #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ]
- entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
- node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
+ node_names = [ v.name for k, v in dir.entries.items()
+ if k not in ('.', '..') ]
names.extend(node_names)
+ if not strings:
+ # Make sure the working directory (self) actually has
+ # entries for all Nodes in repositories or variant dirs.
+ for name in node_names: selfEntry(name)
if ondisk:
try:
disk_names = os.listdir(dir.abspath)
except os.error:
- pass
- else:
- names.extend(disk_names)
- if not strings:
- # We're going to return corresponding Nodes in
- # the local directory, so we need to make sure
- # those Nodes exist. We only want to create
- # Nodes for the entries that will match the
- # specified pattern, though, which means we
- # need to filter the list here, even though
- # the overall list will also be filtered later,
- # after we exit this loop.
- if pattern[0] != '.':
- #disk_names = [ d for d in disk_names if d[0] != '.' ]
- disk_names = filter(lambda x: x[0] != '.', disk_names)
- disk_names = fnmatch.filter(disk_names, pattern)
- rep_nodes = map(dir.Entry, disk_names)
- #rep_nodes = [ n.disambiguate() for n in rep_nodes ]
- rep_nodes = map(lambda n: n.disambiguate(), rep_nodes)
- for node, name in zip(rep_nodes, disk_names):
- n = self.Entry(name)
- if n.__class__ != node.__class__:
- n.__class__ = node.__class__
- n._morph()
+ continue
+ names.extend(disk_names)
+ if not strings:
+ # We're going to return corresponding Nodes in
+ # the local directory, so we need to make sure
+ # those Nodes exist. We only want to create
+ # Nodes for the entries that will match the
+ # specified pattern, though, which means we
+ # need to filter the list here, even though
+ # the overall list will also be filtered later,
+ # after we exit this loop.
+ if pattern[0] != '.':
+ #disk_names = [ d for d in disk_names if d[0] != '.' ]
+ disk_names = [x for x in disk_names if x[0] != '.']
+ disk_names = fnmatch.filter(disk_names, pattern)
+ dirEntry = dir.Entry
+ for name in disk_names:
+ # Add './' before disk filename so that '#' at
+ # beginning of filename isn't interpreted.
+ name = './' + name
+ node = dirEntry(name).disambiguate()
+ n = selfEntry(name)
+ if n.__class__ != node.__class__:
+ n.__class__ = node.__class__
+ n._morph()
names = set(names)
if pattern[0] != '.':
#names = [ n for n in names if n[0] != '.' ]
- names = filter(lambda x: x[0] != '.', names)
+ names = [x for x in names if x[0] != '.']
names = fnmatch.filter(names, pattern)
if strings:
return names
#return [ self.entries[_my_normcase(n)] for n in names ]
- return map(lambda n, e=self.entries: e[_my_normcase(n)], names)
+ return [self.entries[_my_normcase(n)] for n in names]
class RootDir(Dir):
"""A class for the root directory of a file system.
# except for the "lookup abspath," which does not have the
# drive letter.
self.abspath = name + os.sep
- self.labspath = '/'
+ self.labspath = ''
self.path = name + os.sep
self.tpath = name + os.sep
self._morph()
# os.path.normpath() seems to preserve double slashes at the
# beginning of a path (presumably for UNC path names), but
# collapses triple slashes to a single slash.
+ self._lookupDict[''] = self
self._lookupDict['/'] = self
self._lookupDict['//'] = self
self._lookupDict[os.sep] = self
result = self._lookupDict[k]
except KeyError:
if not create:
- raise SCons.Errors.UserError
+ msg = "No such file or directory: '%s' in '%s' (and create is False)" % (p, str(self))
+ raise SCons.Errors.UserError, msg
# There is no Node for this path name, and we're allowed
# to create it.
dir_name, file_name = os.path.split(p)
dir_node = self._lookup_abs(dir_name, Dir)
result = klass(file_name, dir_node, self.fs)
- self._lookupDict[k] = result
- dir_node.entries[_my_normcase(file_name)] = result
- dir_node.implicit = None
# Double-check on disk (as configured) that the Node we
# created matches whatever is out there in the real world.
result.diskcheck_match()
+
+ self._lookupDict[k] = result
+ dir_node.entries[_my_normcase(file_name)] = result
+ dir_node.implicit = None
else:
# There is already a Node for this path name. Allow it to
# complain if we were looking for an inappropriate type.
return self.abspath + name
def entry_labspath(self, name):
- return self.labspath + name
+ return '/' + name
def entry_path(self, name):
return self.path + name
except AttributeError:
s = str(n)
else:
- s = string.replace(s, os.sep, '/')
+ s = s.replace(os.sep, '/')
return s
for attr in ['bsources', 'bdepends', 'bimplicit']:
try:
except AttributeError:
pass
else:
- setattr(self, attr, map(node_to_str, val))
+ setattr(self, attr, list(map(node_to_str, val)))
def convert_from_sconsign(self, dir, name):
"""
Converts a newly-read FileBuildInfo object for in-SCons use
strings = getattr(self, nattr)
nodeinfos = getattr(self, sattr)
except AttributeError:
- pass
- else:
- nodes = []
- for s, ni in zip(strings, nodeinfos):
- if not isinstance(s, SCons.Node.Node):
- s = ni.str_to_node(s)
- nodes.append(s)
- setattr(self, nattr, nodes)
+ continue
+ nodes = []
+ for s, ni in izip(strings, nodeinfos):
+ if not isinstance(s, SCons.Node.Node):
+ s = ni.str_to_node(s)
+ nodes.append(s)
+ setattr(self, nattr, nodes)
def format(self, names=0):
result = []
bkids = self.bsources + self.bdepends + self.bimplicit
bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
- for bkid, bkidsig in zip(bkids, bkidsigs):
+ for bkid, bkidsig in izip(bkids, bkidsigs):
result.append(str(bkid) + ': ' +
- string.join(bkidsig.format(names=names), ' '))
+ ' '.join(bkidsig.format(names=names)))
result.append('%s [%s]' % (self.bactsig, self.bact))
- return string.join(result, '\n')
+ return '\n'.join(result)
class File(Base):
"""A class for files in a file system.
NodeInfo = FileNodeInfo
BuildInfo = FileBuildInfo
+ md5_chunksize = 64
+
def diskcheck_match(self):
diskcheck_match(self, self.isdir,
"Directory %s found where file expected.")
def Entry(self, name):
"""Create an entry node named 'name' relative to
- the SConscript directory of this file."""
- return self.cwd.Entry(name)
+ the directory of this file."""
+ return self.dir.Entry(name)
def Dir(self, name, create=True):
"""Create a directory node named 'name' relative to
- the SConscript directory of this file."""
- return self.cwd.Dir(name, create)
+ the directory of this file."""
+ return self.dir.Dir(name, create=create)
def Dirs(self, pathlist):
"""Create a list of directories relative to the SConscript
directory of this file."""
- return map(lambda p, s=self: s.Dir(p), pathlist)
+ # TODO(1.5)
+ # return [self.Dir(p) for p in pathlist]
+ return [self.Dir(p) for p in pathlist]
def File(self, name):
"""Create a file node named 'name' relative to
- the SConscript directory of this file."""
- return self.cwd.File(name)
+ the directory of this file."""
+ return self.dir.File(name)
#def generate_build_dict(self):
# """Return an appropriate dictionary of values for building
return ''
fname = self.rfile().abspath
try:
- r = open(fname, "rb").read()
+ contents = open(fname, "rb").read()
except EnvironmentError, e:
if not e.filename:
e.filename = fname
raise
- return r
+ return contents
+
+ try:
+ import codecs
+ except ImportError:
+ get_text_contents = get_contents
+ else:
+ # This attempts to figure out what the encoding of the text is
+ # based upon the BOM bytes, and then decodes the contents so that
+ # it's a valid python string.
+ def get_text_contents(self):
+ contents = self.get_contents()
+ # The behavior of various decode() methods and functions
+ # w.r.t. the initial BOM bytes is different for different
+ # encodings and/or Python versions. ('utf-8' does not strip
+ # them, but has a 'utf-8-sig' which does; 'utf-16' seems to
+ # strip them; etc.) Just side step all the complication by
+ # explicitly stripping the BOM before we decode().
+ if contents.startswith(codecs.BOM_UTF8):
+ contents = contents[len(codecs.BOM_UTF8):]
+ # TODO(2.2): Remove when 2.3 becomes floor.
+ #contents = contents.decode('utf-8')
+ contents = my_decode(contents, 'utf-8')
+ elif contents.startswith(codecs.BOM_UTF16_LE):
+ contents = contents[len(codecs.BOM_UTF16_LE):]
+ # TODO(2.2): Remove when 2.3 becomes floor.
+ #contents = contents.decode('utf-16-le')
+ contents = my_decode(contents, 'utf-16-le')
+ elif contents.startswith(codecs.BOM_UTF16_BE):
+ contents = contents[len(codecs.BOM_UTF16_BE):]
+ # TODO(2.2): Remove when 2.3 becomes floor.
+ #contents = contents.decode('utf-16-be')
+ contents = my_decode(contents, 'utf-16-be')
+ return contents
+
+ def get_content_hash(self):
+ """
+ Compute and return the MD5 hash for this file.
+ """
+ if not self.rexists():
+ return SCons.Util.MD5signature('')
+ fname = self.rfile().abspath
+ try:
+ cs = SCons.Util.MD5filesignature(fname,
+ chunksize=SCons.Node.FS.File.md5_chunksize*1024)
+ except EnvironmentError, e:
+ if not e.filename:
+ e.filename = fname
+ raise
+ return cs
+
memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
# This accomodates "chained builds" where a file that's a target
# in one build (SConstruct file) is a source in a different build.
# See test/chained-build.py for the use case.
- self.dir.sconsign().store_info(self.name, self)
+ if do_store_info:
+ self.dir.sconsign().store_info(self.name, self)
convert_copy_attrs = [
'bsources',
try:
value = getattr(old_entry, attr)
except AttributeError:
- pass
- else:
- setattr(binfo, attr, value)
- delattr(old_entry, attr)
+ continue
+ setattr(binfo, attr, value)
+ delattr(old_entry, attr)
for attr in self.convert_sig_attrs:
try:
sig_list = getattr(old_entry, attr)
except AttributeError:
- pass
- else:
- value = []
- for sig in sig_list:
- ninfo = self.new_ninfo()
- if len(sig) == 32:
- ninfo.csig = sig
- else:
- ninfo.timestamp = sig
- value.append(ninfo)
- setattr(binfo, attr, value)
- delattr(old_entry, attr)
+ continue
+ value = []
+ for sig in sig_list:
+ ninfo = self.new_ninfo()
+ if len(sig) == 32:
+ ninfo.csig = sig
+ else:
+ ninfo.timestamp = sig
+ value.append(ninfo)
+ setattr(binfo, attr, value)
+ delattr(old_entry, attr)
return new_entry
memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
try:
sconsign_entry = self.dir.sconsign().get_entry(self.name)
- except (KeyError, OSError):
+ except (KeyError, EnvironmentError):
import SCons.SConsign
sconsign_entry = SCons.SConsign.SConsignEntry()
sconsign_entry.binfo = self.new_binfo()
pass
if scanner:
+ # result = [n.disambiguate() for n in scanner(self, env, path)]
result = scanner(self, env, path)
- result = map(lambda N: N.disambiguate(), result)
+ result = [N.disambiguate() for N in result]
else:
result = []
# created.
self.dir._create()
+ def push_to_cache(self):
+ """Try to push the node into a cache
+ """
+ # This should get called before the Nodes' .built() method is
+ # called, which would clear the build signature if the file has
+ # a source scanner.
+ #
+ # We have to clear the local memoized values *before* we push
+ # the node to cache so that the memoization of the self.exists()
+ # return value doesn't interfere.
+ if self.nocache:
+ return
+ self.clear_memoized_values()
+ if self.exists():
+ self.get_build_env().get_CacheDir().push(self)
+
def retrieve_from_cache(self):
"""Try to retrieve the node's content from a cache
return None
return self.get_build_env().get_CacheDir().retrieve(self)
- def built(self):
- """
- Called just after this node is successfully built.
- """
- # Push this file out to cache before the superclass Node.built()
- # method has a chance to clear the build signature, which it
- # will do if this file has a source scanner.
- #
- # We have to clear the memoized values *before* we push it to
- # cache so that the memoization of the self.exists() return
- # value doesn't interfere.
- self.clear_memoized_values()
- if self.exists():
- self.get_build_env().get_CacheDir().push(self)
- SCons.Node.Node.built(self)
-
def visited(self):
if self.exists():
self.get_build_env().get_CacheDir().push_if_forced(self)
ninfo = self.get_ninfo()
- old = self.get_stored_info()
-
- csig = None
- mtime = self.get_timestamp()
- size = self.get_size()
-
- max_drift = self.fs.max_drift
- if max_drift > 0:
- if (time.time() - mtime) > max_drift:
- try:
- n = old.ninfo
- if n.timestamp and n.csig and n.timestamp == mtime:
- csig = n.csig
- except AttributeError:
- pass
- elif max_drift == 0:
- try:
- csig = old.ninfo.csig
- except AttributeError:
- pass
+ csig = self.get_max_drift_csig()
if csig:
ninfo.csig = csig
- ninfo.timestamp = mtime
- ninfo.size = size
+ ninfo.timestamp = self.get_timestamp()
+ ninfo.size = self.get_size()
if not self.has_builder():
# This is a source file, but it might have been a target file
# in another build that included more of the DAG. Copy
# any build information that's stored in the .sconsign file
# into our binfo object so it doesn't get lost.
+ old = self.get_stored_info()
self.get_binfo().__dict__.update(old.binfo.__dict__)
self.store_info()
scb = self.sbuilder
except AttributeError:
scb = self.sbuilder = self.find_src_builder()
- return not scb is None
+ return scb is not None
def alter_targets(self):
- """Return any corresponding targets in a build directory.
+ """Return any corresponding targets in a variant directory.
"""
if self.is_derived():
return [], None
- return self.fs.build_dir_target_climb(self, self.dir, [self.name])
+ return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
def _rmv_existing(self):
self.clear_memoized_values()
# Duplicate from source path if we are set up to do this.
if self.duplicate and not self.is_derived() and not self.linked:
src = self.srcnode()
- if not src is self:
- # At this point, src is meant to be copied in a build directory.
+ if src is not self:
+ # At this point, src is meant to be copied in a variant directory.
src = src.rfile()
if src.abspath != self.abspath:
if src.exists():
# not actually occur if the -n option is being used.
else:
# The source file does not exist. Make sure no old
- # copy remains in the build directory.
+ # copy remains in the variant directory.
if Base.exists(self) or self.islink():
self.fs.unlink(self.path)
# Return None explicitly because the Base.exists() call
# SIGNATURE SUBSYSTEM
#
+ def get_max_drift_csig(self):
+ """
+ Returns the content signature currently stored for this node
+ if it's been unmodified longer than the max_drift value, or the
+ max_drift value is 0. Returns None otherwise.
+ """
+ old = self.get_stored_info()
+ mtime = self.get_timestamp()
+
+ max_drift = self.fs.max_drift
+ if max_drift > 0:
+ if (time.time() - mtime) > max_drift:
+ try:
+ n = old.ninfo
+ if n.timestamp and n.csig and n.timestamp == mtime:
+ return n.csig
+ except AttributeError:
+ pass
+ elif max_drift == 0:
+ try:
+ return old.ninfo.csig
+ except AttributeError:
+ pass
+
+ return None
+
def get_csig(self):
"""
Generate a node's content signature, the digested signature
except AttributeError:
pass
- try:
- contents = self.get_contents()
- except IOError:
- # This can happen if there's actually a directory on-disk,
- # which can be the case if they've disabled disk checks,
- # or if an action with a File target actually happens to
- # create a same-named directory by mistake.
- csig = ''
- else:
- csig = SCons.Util.MD5signature(contents)
+ csig = self.get_max_drift_csig()
+ if csig is None:
+
+ try:
+ if self.get_size() < SCons.Node.FS.File.md5_chunksize:
+ contents = self.get_contents()
+ else:
+ csig = self.get_content_hash()
+ except IOError:
+ # This can happen if there's actually a directory on-disk,
+ # which can be the case if they've disabled disk checks,
+ # or if an action with a File target actually happens to
+ # create a same-named directory by mistake.
+ csig = ''
+ else:
+ if not csig:
+ csig = SCons.Util.MD5signature(contents)
ninfo.csig = csig
return 1
def changed_state(self, target, prev_ni):
- return (self.state != SCons.Node.up_to_date)
+ return self.state != SCons.Node.up_to_date
def changed_timestamp_then_content(self, target, prev_ni):
if not self.changed_timestamp_match(target, prev_ni):
(isinstance(node, File) or isinstance(node, Entry) \
or not node.is_derived()):
result = node
+ # Copy over our local attributes to the repository
+ # Node so we identify shared object files in the
+ # repository and don't assume they're static.
+ #
+ # This isn't perfect; the attribute would ideally
+ # be attached to the object in the repository in
+ # case it was built statically in the repository
+ # and we changed it to shared locally, but that's
+ # rarely the case and would only occur if you
+ # intentionally used the same suffix for both
+ # shared and static objects anyway. So this
+ # should work well in practice.
+ result.attributes = self.attributes
break
self._memo['rfile'] = result
return result
cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
if not self.exists() and cachefile and os.path.exists(cachefile):
- contents = open(cachefile, 'rb').read()
- self.cachedir_csig = SCons.Util.MD5signature(contents)
+ self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
+ SCons.Node.FS.File.md5_chunksize * 1024)
else:
self.cachedir_csig = self.get_csig()
return self.cachedir_csig
# Add the path to the cache signature, because multiple
# targets built by the same action will all have the same
# build signature, and we have to differentiate them somehow.
- children = self.children()
- sigs = map(lambda n: n.get_cachedir_csig(), children)
+ children = self.children()
executor = self.get_executor()
+ # sigs = [n.get_cachedir_csig() for n in children]
+ sigs = [n.get_cachedir_csig() for n in children]
sigs.append(SCons.Util.MD5signature(executor.get_contents()))
sigs.append(self.path)
- self.cachesig = SCons.Util.MD5collect(sigs)
- return self.cachesig
+ result = self.cachesig = SCons.Util.MD5collect(sigs)
+ return result
+
default_fs = None
It would be more compact to just use this as a nested function
with a default keyword argument (see the commented-out version
below), but that doesn't work unless you have nested scopes,
- so we define it here just this works work under Python 1.5.2.
+ so we define it here just so this work under Python 1.5.2.
"""
if fd is None:
fd = self.default_filedir
dir, name = os.path.split(fd)
drive, d = os.path.splitdrive(dir)
- if d in ('/', os.sep):
- return p
+ if not name and d[:1] in ('/', os.sep):
+ #return p.fs.get_root(drive).dir_on_disk(name)
+ return p.fs.get_root(drive)
if dir:
p = self.filedir_lookup(p, dir)
if not p:
node = p.entries[norm_name]
except KeyError:
return p.dir_on_disk(name)
- # Once we move to Python 2.2 we can do:
- #if isinstance(node, (Dir, Entry)):
- if isinstance(node, Dir) or isinstance(node, Entry):
+ if isinstance(node, Dir):
+ return node
+ if isinstance(node, Entry):
+ node.must_be_same(Dir)
return node
return None
except KeyError:
pass
- if verbose:
+ if verbose and not callable(verbose):
if not SCons.Util.is_String(verbose):
verbose = "find_file"
- if not callable(verbose):
- verbose = ' %s: ' % verbose
- verbose = lambda s, v=verbose: sys.stdout.write(v + s)
- else:
- verbose = lambda x: x
+ _verbose = ' %s: ' % verbose
+ verbose = lambda s: sys.stdout.write(_verbose + s)
filedir, filename = os.path.split(filename)
if filedir:
# node = p.entries[norm_name]
# except KeyError:
# return p.dir_on_disk(name)
- # # Once we move to Python 2.2 we can do:
- # #if isinstance(node, (Dir, Entry)):
+ # if isinstance(node, Dir):
+ # return node
+ # if isinstance(node, Entry):
+ # node.must_be_same(Dir)
+ # return node
# if isinstance(node, Dir) or isinstance(node, Entry):
# return node
# return None
#paths = filter(None, map(filedir_lookup, paths))
self.default_filedir = filedir
- paths = filter(None, map(self.filedir_lookup, paths))
+ paths = [_f for _f in map(self.filedir_lookup, paths) if _f]
result = None
for dir in paths:
- verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
+ if verbose:
+ verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
node, d = dir.srcdir_find_file(filename)
if node:
- verbose("... FOUND '%s' in '%s'\n" % (filename, d))
+ if verbose:
+ verbose("... FOUND '%s' in '%s'\n" % (filename, d))
result = node
break
return result
find_file = FileFinder().find_file
+
+
+def invalidate_node_memos(targets):
+ """
+ Invalidate the memoized values of all Nodes (files or directories)
+ that are associated with the given entries. Has been added to
+ clear the cache of nodes affected by a direct execution of an
+ action (e.g. Delete/Copy/Chmod). Existing Node caches become
+ inconsistent if the action is run through Execute(). The argument
+ `targets` can be a single Node object or filename, or a sequence
+ of Nodes/filenames.
+ """
+ from traceback import extract_stack
+
+ # First check if the cache really needs to be flushed. Only
+ # actions run in the SConscript with Execute() seem to be
+ # affected. XXX The way to check if Execute() is in the stacktrace
+ # is a very dirty hack and should be replaced by a more sensible
+ # solution.
+ for f in extract_stack():
+ if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
+ break
+ else:
+ # Dont have to invalidate, so return
+ return
+
+ if not SCons.Util.is_List(targets):
+ targets = [targets]
+
+ for entry in targets:
+ # If the target is a Node object, clear the cache. If it is a
+ # filename, look up potentially existing Node object first.
+ try:
+ entry.clear_memoized_values()
+ except AttributeError:
+ # Not a Node object, try to look up Node by filename. XXX
+ # This creates Node objects even for those filenames which
+ # do not correspond to an existing Node object.
+ node = get_default_fs().Entry(entry)
+ if node:
+ node.clear_memoized_values()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: