diff options
Diffstat (limited to 'src/engine/SCons/Node')
| -rw-r--r-- | src/engine/SCons/Node/Alias.py | 4 | ||||
| -rw-r--r-- | src/engine/SCons/Node/AliasTests.py | 17 | ||||
| -rw-r--r-- | src/engine/SCons/Node/FS.py | 325 | ||||
| -rw-r--r-- | src/engine/SCons/Node/FSTests.py | 185 | ||||
| -rw-r--r-- | src/engine/SCons/Node/NodeTests.py | 21 | ||||
| -rw-r--r-- | src/engine/SCons/Node/Python.py | 17 | ||||
| -rw-r--r-- | src/engine/SCons/Node/PythonTests.py | 23 | ||||
| -rw-r--r-- | src/engine/SCons/Node/__init__.py | 81 | 
8 files changed, 519 insertions, 154 deletions
diff --git a/src/engine/SCons/Node/Alias.py b/src/engine/SCons/Node/Alias.py index 567f663..9ea2fe0 100644 --- a/src/engine/SCons/Node/Alias.py +++ b/src/engine/SCons/Node/Alias.py @@ -8,7 +8,7 @@ This creates a hash of global Aliases (dummy targets).  """  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -30,7 +30,7 @@ This creates a hash of global Aliases (dummy targets).  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  # -__revision__ = "src/engine/SCons/Node/Alias.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/Alias.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import collections diff --git a/src/engine/SCons/Node/AliasTests.py b/src/engine/SCons/Node/AliasTests.py index e1929f6..21829b6 100644 --- a/src/engine/SCons/Node/AliasTests.py +++ b/src/engine/SCons/Node/AliasTests.py @@ -1,5 +1,5 @@  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -21,13 +21,11 @@  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  # -__revision__ = "src/engine/SCons/Node/AliasTests.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/AliasTests.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import sys  import unittest -import TestUnit -  import SCons.Errors  import SCons.Node.Alias @@ -113,16 +111,7 @@ class AliasBuildInfoTestCase(unittest.TestCase):          bi = SCons.Node.Alias.AliasBuildInfo()  if __name__ == "__main__": -    suite = unittest.TestSuite() -    tclasses = [ -        AliasTestCase, -        AliasBuildInfoTestCase, -        AliasNodeInfoTestCase, -    ] -    for tclass in tclasses: -        names = unittest.getTestCaseNames(tclass, 'test_') -        suite.addTests(list(map(tclass, names))) -    TestUnit.run(suite) +    unittest.main()  # Local Variables:  # tab-width:4 diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index a953890..6319ace 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -11,7 +11,7 @@ that can be used by scripts or modules looking for the canonical default.  """  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -33,7 +33,7 @@ that can be used by scripts or modules looking for the canonical default.  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  from __future__ import print_function -__revision__ = "src/engine/SCons/Node/FS.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/FS.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import fnmatch  import os @@ -43,6 +43,7 @@ import stat  import sys  import time  import codecs +from itertools import chain  import SCons.Action  import SCons.Debug @@ -56,9 +57,12 @@ import SCons.Util  import SCons.Warnings  from SCons.Debug import Trace +from . import DeciderNeedsNode  print_duplicate = 0 +MD5_TIMESTAMP_DEBUG = False +  def sconsign_none(node):      raise NotImplementedError @@ -74,6 +78,9 @@ def sconsign_dir(node):  _sconsign_map = {0 : sconsign_none,                   1 : sconsign_dir} +class FileBuildInfoFileToCsigMappingError(Exception): +    pass +  class EntryProxyAttributeError(AttributeError):      """      An AttributeError subclass for recording and displaying the name @@ -132,7 +139,10 @@ def initialize_do_splitdrive():      global do_splitdrive      global has_unc      drive, path = os.path.splitdrive('X:/foo') -    has_unc = hasattr(os.path, 'splitunc') +    # splitunc is removed from python 3.7 and newer +    # so we can also just test if splitdrive works with UNC +    has_unc = (hasattr(os.path, 'splitunc') +        or os.path.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive')      do_splitdrive = not not drive or has_unc @@ -282,11 +292,13 @@ def set_duplicate(duplicate):              Link_Funcs.append(link_dict[func])  def LinkFunc(target, source, env): -    # Relative paths cause problems with symbolic links, so -    # we use absolute paths, which may be a problem for people -    # who want to move their soft-linked src-trees around. Those -    # people should use the 'hard-copy' mode, softlinks cannot be -    # used for that; at least I have no idea how ... +    """ +    Relative paths cause problems with symbolic links, so +    we use absolute paths, which may be a problem for people +    who want to move their soft-linked src-trees around. Those +    people should use the 'hard-copy' mode, softlinks cannot be +    used for that; at least I have no idea how ... +    """      src = source[0].get_abspath()      dest = target[0].get_abspath()      dir, file = os.path.split(dest) @@ -328,7 +340,12 @@ Unlink = SCons.Action.Action(UnlinkFunc, None)  def MkdirFunc(target, source, env):      t = target[0] -    if not t.exists(): +    # This os.path.exists test looks redundant, but it's possible +    # when using Install() to install multiple dirs outside the +    # source tree to get a case where t.exists() is true but +    # the path does already exist, so this prevents spurious +    # build failures in that case. See test/Install/multi-dir. +    if not t.exists() and not os.path.exists(t.get_abspath()):          t.fs.mkdir(t.get_abspath())      return 0 @@ -682,10 +699,15 @@ class Base(SCons.Node.Node):      @SCons.Memoize.CountMethodCall      def stat(self): -        try: return self._memo['stat'] -        except KeyError: pass -        try: result = self.fs.stat(self.get_abspath()) -        except os.error: result = None +        try:  +            return self._memo['stat'] +        except KeyError:  +            pass +        try:  +            result = self.fs.stat(self.get_abspath()) +        except os.error:  +            result = None +          self._memo['stat'] = result          return result @@ -697,13 +719,17 @@ class Base(SCons.Node.Node):      def getmtime(self):          st = self.stat() -        if st: return st[stat.ST_MTIME] -        else: return None +        if st:  +            return st[stat.ST_MTIME] +        else:  +            return None      def getsize(self):          st = self.stat() -        if st: return st[stat.ST_SIZE] -        else: return None +        if st:  +            return st[stat.ST_SIZE] +        else:  +            return None      def isdir(self):          st = self.stat() @@ -1048,21 +1074,22 @@ _classEntry = Entry  class LocalFS(object): - -    # This class implements an abstraction layer for operations involving -    # a local file system.  Essentially, this wraps any function in -    # the os, os.path or shutil modules that we use to actually go do -    # anything with or to the local file system. -    # -    # Note that there's a very good chance we'll refactor this part of -    # the architecture in some way as we really implement the interface(s) -    # for remote file system Nodes.  For example, the right architecture -    # might be to have this be a subclass instead of a base class. -    # Nevertheless, we're using this as a first step in that direction. -    # -    # We're not using chdir() yet because the calling subclass method -    # needs to use os.chdir() directly to avoid recursion.  Will we -    # really need this one? +    """ +    This class implements an abstraction layer for operations involving +    a local file system.  Essentially, this wraps any function in +    the os, os.path or shutil modules that we use to actually go do +    anything with or to the local file system. + +    Note that there's a very good chance we'll refactor this part of +    the architecture in some way as we really implement the interface(s) +    for remote file system Nodes.  For example, the right architecture +    might be to have this be a subclass instead of a base class. +    Nevertheless, we're using this as a first step in that direction. + +    We're not using chdir() yet because the calling subclass method +    needs to use os.chdir() directly to avoid recursion.  Will we +    really need this one? +    """      #def chdir(self, path):      #    return os.chdir(path)      def chmod(self, path, mode): @@ -1389,10 +1416,10 @@ class FS(LocalFS):              if not isinstance(d, SCons.Node.Node):                  d = self.Dir(d)              self.Top.addRepository(d) -     +      def PyPackageDir(self, modulename):          """Locate the directory of a given python module name -		 +          For example scons might resolve to          Windows: C:\Python27\Lib\site-packages\scons-2.5.1          Linux: /usr/lib/scons @@ -2462,11 +2489,42 @@ class FileNodeInfo(SCons.Node.NodeInfoBase):              if key not in ('__weakref__',):                  setattr(self, key, value) +    def __eq__(self, other): +        return self.csig == other.csig and self.timestamp == other.timestamp and self.size == other.size + +    def __ne__(self, other): +        return not self.__eq__(other) +  class FileBuildInfo(SCons.Node.BuildInfoBase): -    __slots__ = () +    """ +    This is info loaded from sconsign. + +    Attributes unique to FileBuildInfo: +        dependency_map : Caches file->csig mapping +                    for all dependencies.  Currently this is only used when using +                    MD5-timestamp decider. +                    It's used to ensure that we copy the correct +                    csig from previous build to be written to .sconsign when current build +                    is done. Previously the matching of csig to file was strictly by order +                    they appeared in bdepends, bsources, or bimplicit, and so a change in order +                    or count of any of these could yield writing wrong csig, and then false positive +                    rebuilds +    """ +    __slots__ = ('dependency_map')      current_version_id = 2 +    def __setattr__(self, key, value): + +        # If any attributes are changed in FileBuildInfo, we need to +        # invalidate the cached map of file name to content signature +        # heald in dependency_map. Currently only used with +        # MD5-timestamp decider +        if key != 'dependency_map' and hasattr(self, 'dependency_map'): +            del self.dependency_map + +        return super(FileBuildInfo, self).__setattr__(key, value) +      def convert_to_sconsign(self):          """          Converts this FileBuildInfo object for writing to a .sconsign file @@ -3235,14 +3293,155 @@ class File(Base):      def changed_state(self, target, prev_ni):          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): + +    # Caching node -> string mapping for the below method +    __dmap_cache = {} +    __dmap_sig_cache = {} + + +    def _build_dependency_map(self, binfo): +        """ +        Build mapping from file -> signature + +        Args: +            self - self +            binfo - buildinfo from node being considered + +        Returns: +            dictionary of file->signature mappings +        """ + +        # For an "empty" binfo properties like bsources +        # do not exist: check this to avoid exception. +        if (len(binfo.bsourcesigs) + len(binfo.bdependsigs) + \ +            len(binfo.bimplicitsigs)) == 0: +            return {} + + +        # store this info so we can avoid regenerating it. +        binfo.dependency_map = { str(child):signature for child, signature in zip(chain(binfo.bsources, binfo.bdepends, binfo.bimplicit), +                                     chain(binfo.bsourcesigs, binfo.bdependsigs, binfo.bimplicitsigs))} + +        return binfo.dependency_map + +    def _get_previous_signatures(self, dmap): +        """ +        Return a list of corresponding csigs from previous +        build in order of the node/files in children. + +        Args: +            self - self +            dmap - Dictionary of file -> csig + +        Returns: +            List of csigs for provided list of children +        """ +        prev = [] +        # MD5_TIMESTAMP_DEBUG = False + +        if len(dmap) == 0: +            if MD5_TIMESTAMP_DEBUG: print("Nothing dmap shortcutting") +            return None + +        if MD5_TIMESTAMP_DEBUG: print("len(dmap):%d"%len(dmap)) +        # First try the simple name for node +        c_str = str(self) +        if MD5_TIMESTAMP_DEBUG: print("Checking   :%s"%c_str) +        df = dmap.get(c_str, None) +        if df: +            return df +         +        if os.altsep: +            c_str = c_str.replace(os.sep, os.altsep) +            df = dmap.get(c_str, None) +            if MD5_TIMESTAMP_DEBUG: print("-->%s"%df) +            if df: +                return df + +        if not df: +            try: +                # this should yield a path which matches what's in the sconsign +                c_str = self.get_path() +                df = dmap.get(c_str, None) +                if MD5_TIMESTAMP_DEBUG: print("-->%s"%df) +                if df: +                    return df + +                if os.altsep: +                    c_str = c_str.replace(os.sep, os.altsep) +                    df = dmap.get(c_str, None) +                    if MD5_TIMESTAMP_DEBUG: print("-->%s"%df) +                    if df: +                        return df + +            except AttributeError as e: +                raise FileBuildInfoFileToCsigMappingError("No mapping from file name to content signature for :%s"%c_str) + +        return df + +    def changed_timestamp_then_content(self, target, prev_ni, node=None): +        """ +        Used when decider for file is Timestamp-MD5 + +        NOTE: If the timestamp hasn't changed this will skip md5'ing the +              file and just copy the prev_ni provided.  If the prev_ni +              is wrong. It will propagate it. +              See: https://github.com/SCons/scons/issues/2980 +         +        Args: +            self - dependency +            target - target +            prev_ni - The NodeInfo object loaded from previous builds .sconsign +            node - Node instance.  This is the only changed* function which requires +                   node to function. So if we detect that it's not passed. +                   we throw DeciderNeedsNode, and caller should handle this and pass node. + +        Returns:  +            Boolean - Indicates if node(File) has changed. +        """ +        if node is None: +            # We need required node argument to get BuildInfo to function +            raise DeciderNeedsNode(self.changed_timestamp_then_content) + +        # Now get sconsign name -> csig map and then get proper prev_ni if possible +        bi = node.get_stored_info().binfo +        rebuilt = False +        try: +            dependency_map = bi.dependency_map +        except AttributeError as e: +            dependency_map = self._build_dependency_map(bi) +            rebuilt = True + +        if len(dependency_map) == 0: +            # If there's no dependency map, there's no need to find the +            # prev_ni as there aren't any +            # shortcut the rest of the logic +            if MD5_TIMESTAMP_DEBUG: print("Skipping checks len(dmap)=0") + +            # We still need to get the current file's csig +            # This should be slightly faster than calling self.changed_content(target, new_prev_ni) +            self.get_csig() +            return True + +        new_prev_ni = self._get_previous_signatures(dependency_map) +        new = self.changed_timestamp_match(target, new_prev_ni) + +        if MD5_TIMESTAMP_DEBUG: +            old = self.changed_timestamp_match(target, prev_ni) + +            if old != new: +                print("Mismatch self.changed_timestamp_match(%s, prev_ni) old:%s new:%s"%(str(target), old, new)) +                new_prev_ni = self._get_previous_signatures(dependency_map) + + +        if not new:              try: -                self.get_ninfo().csig = prev_ni.csig +                # NOTE: We're modifying the current node's csig in a query. +                self.get_ninfo().csig = new_prev_ni.csig              except AttributeError:                  pass              return False -        return self.changed_content(target, prev_ni) +        return self.changed_content(target, new_prev_ni)      def changed_timestamp_newer(self, target, prev_ni):          try: @@ -3251,6 +3450,12 @@ class File(Base):              return 1      def changed_timestamp_match(self, target, prev_ni): +        """ +        Return True if the timestamps don't match or if there is no previous timestamp +        :param target: +        :param prev_ni: Information about the node from the previous build +        :return: +        """          try:              return self.get_timestamp() != prev_ni.timestamp          except AttributeError: @@ -3272,7 +3477,9 @@ class File(Base):                          # ...and they'd like a local copy.                          e = LocalCopy(self, r, None)                          if isinstance(e, SCons.Errors.BuildError): -                            raise +                            # Likely this should be re-raising exception e +                            # (which would be BuildError) +                            raise e                          SCons.Node.store_info_map[self.store_info](self)                      if T: Trace(' 1\n')                      return 1 @@ -3293,11 +3500,14 @@ class File(Base):          result = self          if not self.exists():              norm_name = _my_normcase(self.name) -            for dir in self.dir.get_all_rdirs(): -                try: node = dir.entries[norm_name] -                except KeyError: node = dir.file_on_disk(self.name) +            for repo_dir in self.dir.get_all_rdirs(): +                try: +                    node = repo_dir.entries[norm_name] +                except KeyError: +                    node = repo_dir.file_on_disk(self.name) +                  if node and node.exists() and \ -                   (isinstance(node, File) or isinstance(node, Entry) \ +                   (isinstance(node, File) or isinstance(node, Entry)                      or not node.is_derived()):                          result = node                          # Copy over our local attributes to the repository @@ -3317,6 +3527,28 @@ class File(Base):          self._memo['rfile'] = result          return result +    def find_repo_file(self): +        """ +        For this node, find if there exists a corresponding file in one or more repositories +        :return: list of corresponding files in repositories +        """ +        retvals = [] + +        norm_name = _my_normcase(self.name) +        for repo_dir in self.dir.get_all_rdirs(): +            try: +                node = repo_dir.entries[norm_name] +            except KeyError: +                node = repo_dir.file_on_disk(self.name) + +            if node and node.exists() and \ +                    (isinstance(node, File) or isinstance(node, Entry) \ +                     or not node.is_derived()): +                retvals.append(node) + +        return retvals + +      def rstr(self):          return str(self.rfile()) @@ -3374,6 +3606,8 @@ class File(Base):          because multiple targets built by the same action will all          have the same build signature, and we have to differentiate          them somehow. + +        Signature should normally be string of hex digits.          """          try:              return self.cachesig @@ -3383,10 +3617,13 @@ class File(Base):          # Collect signatures for all children          children = self.children()          sigs = [n.get_cachedir_csig() for n in children] +          # Append this node's signature...          sigs.append(self.get_contents_sig()) +          # ...and it's path          sigs.append(self.get_internal_path()) +          # Merge this all into a single signature          result = self.cachesig = SCons.Util.MD5collect(sigs)          return result diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 3a36894..021d433 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -1,5 +1,5 @@  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -22,7 +22,7 @@  #  from __future__ import division, print_function -__revision__ = "src/engine/SCons/Node/FSTests.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/FSTests.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import SCons.compat @@ -41,6 +41,7 @@ import SCons.Errors  import SCons.Node.FS  import SCons.Util  import SCons.Warnings +import SCons.Environment  built_it = None @@ -605,7 +606,7 @@ class VariantDirTestCase(unittest.TestCase):                  print("File `%s' alter_targets() `%s' != expected `%s'" % (f, tp, expect))                  errors = errors + 1 -        self.failIf(errors) +        self.assertFalse(errors)  class BaseTestCase(_tempdirTestCase):      def test_stat(self): @@ -1133,7 +1134,10 @@ class FSTestCase(_tempdirTestCase):              e1 = fs.Entry(p)              e2 = fs.Entry(path)              assert e1 is e2, (e1, e2) -            assert str(e1) is str(e2), (str(e1), str(e2)) +            a=str(e1) +            b=str(e2) +            assert a == b, ("Strings should match for same file/node\n%s\n%s"%(a,b)) +          # Test for a bug in 0.04 that did not like looking up          # dirs with a trailing slash on Windows. @@ -1657,7 +1661,12 @@ class FSTestCase(_tempdirTestCase):              import ntpath              x = test.workpath(*dirs)              drive, path = ntpath.splitdrive(x) -            unc, path = ntpath.splitunc(path) +            try: +                unc, path = ntpath.splitunc(path) +            except AttributeError: +                # could be python 3.7 or newer, make sure splitdrive can do UNC +                assert ntpath.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive' +                pass              path = strip_slash(path)              return '//' + path[1:] @@ -2455,6 +2464,139 @@ class FileTestCase(_tempdirTestCase):          assert not build_f1.exists(), "%s did not realize that %s disappeared" % (build_f1, src_f1)          assert not os.path.exists(build_f1.get_abspath()), "%s did not get removed after %s was removed" % (build_f1, src_f1) +    def test_changed(self): +        """  +        Verify that changes between BuildInfo's list of souces, depends, and implicit  +        dependencies do not corrupt content signature values written to .SConsign +        when using CacheDir and Timestamp-MD5 decider. +        This is for issue #2980 +        """ +        # node should have +        # 1 source (for example main.cpp) +        # 0 depends +        # N implicits (for example ['alpha.h', 'beta.h', 'gamma.h', '/usr/bin/g++']) + +        class ChangedNode(SCons.Node.FS.File): +            def __init__(self, name, directory=None, fs=None): +                SCons.Node.FS.File.__init__(self, name, directory, fs) +                self.name = name +                self.Tag('found_includes', []) +                self.stored_info = None +                self.build_env = None +                self.changed_since_last_build = 4 +                self.timestamp = 1 + +            def get_stored_info(self): +                return self.stored_info + +            def get_build_env(self): +                return self.build_env + +            def get_timestamp(self): +                """ Fake timestamp so they always match""" +                return self.timestamp + +            def get_contents(self): +                return self.name + +            def get_ninfo(self): +                """ mocked to ensure csig will equal the filename""" +                try: +                    return self.ninfo +                except AttributeError: +                    self.ninfo = FakeNodeInfo(self.name, self.timestamp) +                    return self.ninfo + +            def get_csig(self): +                ninfo = self.get_ninfo() +                try: +                    return ninfo.csig +                except AttributeError: +                    pass + +                return "Should Never Happen" + +        class ChangedEnvironment(SCons.Environment.Base): + +            def __init__(self): +                SCons.Environment.Base.__init__(self) +                self.decide_source = self._changed_timestamp_then_content + +        class FakeNodeInfo(object): +            def __init__(self, csig, timestamp): +                self.csig = csig +                self.timestamp = timestamp + +        #Create nodes +        fs = SCons.Node.FS.FS() +        d = self.fs.Dir('.') + +        node = ChangedNode('main.o',d,fs)  # main.o +        s1 = ChangedNode('main.cpp',d,fs) # main.cpp +        s1.timestamp = 2 # this changed +        i1 = ChangedNode('alpha.h',d,fs) # alpha.h - The bug is caused because the second build adds this file +        i1.timestamp = 2 # This is the new file. +        i2 = ChangedNode('beta.h',d,fs) # beta.h +        i3 = ChangedNode('gamma.h',d,fs) # gamma.h - In the bug beta.h's csig from binfo overwrites this ones +        i4 = ChangedNode('/usr/bin/g++',d,fs) # /usr/bin/g++ + +        node.add_source([s1]) +        node.add_dependency([]) +        node.implicit = [i1, i2, i3, i4] +        node.implicit_set = set() +        # node._add_child(node.implicit, node.implicit_set, [n7, n8, n9]) +        # node._add_child(node.implicit, node.implicit_set, [n10, n11, n12]) + +        # Mock out node's scan method +        # node.scan = lambda *args: None + +        # Mock out nodes' children() ? +        # Should return Node's. +        # All those nodes should have changed_since_last_build set to match Timestamp-MD5's +        # decider method... + +        # Generate sconsign entry needed +        sconsign_entry = SCons.SConsign.SConsignEntry() +        sconsign_entry.binfo = node.new_binfo() +        sconsign_entry.ninfo = node.new_ninfo() + +        # mock out loading info from sconsign +        # This will cause node.get_stored_info() to return our freshly created sconsign_entry +        node.stored_info = sconsign_entry + +        # binfo = information from previous build (from sconsign) +        # We'll set the following attributes (which are lists): "bsources", "bsourcesigs", +        # "bdepends","bdependsigs", "bimplicit", "bimplicitsigs" +        bi = sconsign_entry.binfo +        bi.bsources = ['main.cpp'] +        bi.bsourcesigs=[FakeNodeInfo('main.cpp',1),] + +        bi.bdepends = [] +        bi.bdependsigs = [] + +        bi.bimplicit = ['beta.h','gamma.h'] +        bi.bimplicitsigs = [FakeNodeInfo('beta.h',1), FakeNodeInfo('gamma.h',1)] + +        ni = sconsign_entry.ninfo +        # We'll set the following attributes (which are lists): sources, depends, implicit lists + +        #Set timestamp-md5 +        #Call changed +        #Check results +        node.build_env = ChangedEnvironment() + +        changed = node.changed() + +        # change to true to debug +        if False: +            print("Changed:%s"%changed) +            print("%15s -> csig:%s"%(s1.name, s1.ninfo.csig)) +            print("%15s -> csig:%s"%(i1.name, i1.ninfo.csig)) +            print("%15s -> csig:%s"%(i2.name, i2.ninfo.csig)) +            print("%15s -> csig:%s"%(i3.name, i3.ninfo.csig)) +            print("%15s -> csig:%s"%(i4.name, i4.ninfo.csig)) + +        self.assertEqual(i2.name,i2.ninfo.csig, "gamma.h's fake csig should equal gamma.h but equals:%s"%i2.ninfo.csig)  class GlobTestCase(_tempdirTestCase): @@ -3747,38 +3889,7 @@ class AbsolutePathTestCase(unittest.TestCase):  if __name__ == "__main__": -    suite = unittest.TestSuite() -    suite.addTest(VariantDirTestCase()) -    suite.addTest(find_fileTestCase()) -    suite.addTest(StringDirTestCase()) -    suite.addTest(stored_infoTestCase()) -    suite.addTest(has_src_builderTestCase()) -    suite.addTest(prepareTestCase()) -    suite.addTest(SConstruct_dirTestCase()) -    suite.addTest(clearTestCase()) -    suite.addTest(disambiguateTestCase()) -    suite.addTest(postprocessTestCase()) -    suite.addTest(SpecialAttrTestCase()) -    suite.addTest(SaveStringsTestCase()) -    tclasses = [ -        AbsolutePathTestCase, -        BaseTestCase, -        CacheDirTestCase, -        DirTestCase, -        DirBuildInfoTestCase, -        DirNodeInfoTestCase, -        EntryTestCase, -        FileTestCase, -        FileBuildInfoTestCase, -        FileNodeInfoTestCase, -        FSTestCase, -        GlobTestCase, -        RepositoryTestCase, -    ] -    for tclass in tclasses: -        names = unittest.getTestCaseNames(tclass, 'test_') -        suite.addTests(list(map(tclass, names))) -    TestUnit.run(suite) +    unittest.main()  # Local Variables:  # tab-width:4 diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index e50616a..39b928b 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -1,5 +1,5 @@  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -20,7 +20,7 @@  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -__revision__ = "src/engine/SCons/Node/NodeTests.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/NodeTests.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import SCons.compat @@ -30,8 +30,6 @@ import re  import sys  import unittest -import TestUnit -  import SCons.Errors  import SCons.Node  import SCons.Util @@ -1337,9 +1335,9 @@ class NodeListTestCase(unittest.TestCase):          assert s == "['n3', 'n2', 'n1']", s          r = repr(nl) -        r = re.sub('at (0[xX])?[0-9a-fA-F]+', 'at 0x', r) +        r = re.sub(r'at (0[xX])?[0-9a-fA-F]+', 'at 0x', r)          # Don't care about ancestry: just leaf value of MyNode -        r = re.sub('<.*?\.MyNode', '<MyNode', r) +        r = re.sub(r'<.*?\.MyNode', '<MyNode', r)          # New-style classes report as "object"; classic classes report          # as "instance"...          r = re.sub("object", "instance", r) @@ -1349,15 +1347,8 @@ class NodeListTestCase(unittest.TestCase):  if __name__ == "__main__": -    suite = unittest.TestSuite() -    tclasses = [ BuildInfoBaseTestCase, -                 NodeInfoBaseTestCase, -                 NodeTestCase, -                 NodeListTestCase ] -    for tclass in tclasses: -        names = unittest.getTestCaseNames(tclass, 'test_') -        suite.addTests(list(map(tclass, names))) -    TestUnit.run(suite) +    unittest.main() +  # Local Variables:  # tab-width:4 diff --git a/src/engine/SCons/Node/Python.py b/src/engine/SCons/Node/Python.py index 06cb93a..30daf9a 100644 --- a/src/engine/SCons/Node/Python.py +++ b/src/engine/SCons/Node/Python.py @@ -5,7 +5,7 @@ Python nodes.  """  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -27,7 +27,7 @@ Python nodes.  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  # -__revision__ = "src/engine/SCons/Node/Python.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/Python.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import SCons.Node @@ -137,6 +137,10 @@ class Value(SCons.Node.Node):          return contents      def get_contents(self): +        """ +        Get contents for signature calculations. +        :return: bytes +        """          text_contents = self.get_text_contents()          try:              return text_contents.encode() @@ -155,12 +159,17 @@ class Value(SCons.Node.Node):      def get_csig(self, calc=None):          """Because we're a Python value node and don't have a real          timestamp, we get to ignore the calculator and just use the -        value contents.""" +        value contents. + +        Returns string. Ideally string of hex digits. (Not bytes) +        """          try:              return self.ninfo.csig          except AttributeError:              pass -        contents = self.get_contents() + +        contents = self.get_text_contents() +          self.get_ninfo().csig = contents          return contents diff --git a/src/engine/SCons/Node/PythonTests.py b/src/engine/SCons/Node/PythonTests.py index aab7e85..2b35f00 100644 --- a/src/engine/SCons/Node/PythonTests.py +++ b/src/engine/SCons/Node/PythonTests.py @@ -1,5 +1,5 @@  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -21,13 +21,11 @@  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  # -__revision__ = "src/engine/SCons/Node/PythonTests.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/PythonTests.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog"  import sys  import unittest -import TestUnit -  import SCons.Errors  import SCons.Node.Python @@ -90,15 +88,15 @@ class ValueTestCase(unittest.TestCase):          """          v1 = SCons.Node.Python.Value('aaa')          csig = v1.get_csig(None) -        assert csig.decode() == 'aaa', csig +        assert csig == 'aaa', csig          v2 = SCons.Node.Python.Value(7)          csig = v2.get_csig(None) -        assert csig.decode() == '7', csig +        assert csig == '7', csig          v3 = SCons.Node.Python.Value(None)          csig = v3.get_csig(None) -        assert csig.decode() == 'None', csig +        assert csig == 'None', csig  class ValueNodeInfoTestCase(unittest.TestCase):      def test___init__(self): @@ -113,16 +111,7 @@ class ValueBuildInfoTestCase(unittest.TestCase):          bi = SCons.Node.Python.ValueBuildInfo()  if __name__ == "__main__": -    suite = unittest.TestSuite() -    tclasses = [ -        ValueTestCase, -        ValueBuildInfoTestCase, -        ValueNodeInfoTestCase, -    ] -    for tclass in tclasses: -        names = unittest.getTestCaseNames(tclass, 'test_') -        suite.addTests(list(map(tclass, names))) -    TestUnit.run(suite) +    unittest.main()  # Local Variables:  # tab-width:4 diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 793e101..564e8de 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -22,7 +22,7 @@ be able to depend on any other type of "thing."  from __future__ import print_function  # -# Copyright (c) 2001 - 2017 The SCons Foundation +# Copyright (c) 2001 - 2019 The SCons Foundation  #  # Permission is hereby granted, free of charge, to any person obtaining  # a copy of this software and associated documentation files (the @@ -43,8 +43,9 @@ from __future__ import print_function  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -__revision__ = "src/engine/SCons/Node/__init__.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog" +__revision__ = "src/engine/SCons/Node/__init__.py 103260fce95bf5db1c35fb2371983087d85dd611 2019-07-13 18:25:30 bdbaddog" +import os  import collections  import copy  from itertools import chain @@ -139,6 +140,7 @@ def exists_entry(node):      node.disambiguate()      return _exists_map[node._func_exists](node) +  def exists_file(node):      # Duplicate from source path if we are set up to do this.      if node.duplicate and not node.is_derived() and not node.linked: @@ -155,7 +157,7 @@ def exists_file(node):                      # The source file does not exist.  Make sure no old                      # copy remains in the variant directory.                      if print_duplicate: -                        print("dup: no src for %s, unlinking old variant copy"%self) +                        print("dup: no src for %s, unlinking old variant copy" % node)                      if exists_base(node) or node.islink():                          node.fs.unlink(node.get_internal_path())                      # Return None explicitly because the Base.exists() call @@ -246,6 +248,21 @@ _target_from_source_map = {0 : target_from_source_none,  # used by it.  # + +class DeciderNeedsNode(Exception): +    """ +    Indicate that the decider needs the node as well as the target and the dependency. +    Normally the node and the target are the same, but in the case of repository +    They may be different. Also the NodeInfo is retrieved from the node +    """ +    def __init__(self, call_this_decider): +        """ +        :param call_this_decider: to return the decider to call directly since deciders +               are called through several levels of indirection +        """ +        self.decider = call_this_decider + +  #  # First, the single decider functions  # @@ -269,6 +286,7 @@ def changed_since_last_build_node(node, target, prev_ni):      """      raise NotImplementedError +  def changed_since_last_build_alias(node, target, prev_ni):      cur_csig = node.get_csig()      try: @@ -276,19 +294,24 @@ def changed_since_last_build_alias(node, target, prev_ni):      except AttributeError:          return 1 +  def changed_since_last_build_entry(node, target, prev_ni):      node.disambiguate()      return _decider_map[node.changed_since_last_build](node, target, prev_ni) +  def changed_since_last_build_state_changed(node, target, prev_ni): -    return (node.state != SCons.Node.up_to_date) +    return node.state != SCons.Node.up_to_date +  def decide_source(node, target, prev_ni):      return target.get_build_env().decide_source(node, target, prev_ni) +  def decide_target(node, target, prev_ni):      return target.get_build_env().decide_target(node, target, prev_ni) +  def changed_since_last_build_python(node, target, prev_ni):      cur_csig = node.get_csig()      try: @@ -380,6 +403,7 @@ class NodeInfoBase(object):          """          state = other.__getstate__()          self.__setstate__(state) +      def format(self, field_list=None, names=0):          if field_list is None:              try: @@ -665,7 +689,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):                  executor.cleanup()      def reset_executor(self): -        "Remove cached executor; forces recompute when needed." +        """Remove cached executor; forces recompute when needed."""          try:              delattr(self, 'executor')          except AttributeError: @@ -1136,7 +1160,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):              binfo.bactsig = SCons.Util.MD5signature(executor.get_contents())          if self._specific_sources: -            sources = [ s for s in self.sources if not s in ignore_set] +            sources = [s for s in self.sources if not s in ignore_set]          else:              sources = executor.get_unignored_sources(self, self.ignore) @@ -1145,13 +1169,17 @@ class Node(object, with_metaclass(NoSlotsPyPy)):          binfo.bsources = [s for s in sources if s not in seen and not seen.add(s)]          binfo.bsourcesigs = [s.get_ninfo() for s in binfo.bsources] +        binfo.bdepends = [d for d in self.depends if d not in ignore_set] +        binfo.bdependsigs = [d.get_ninfo() for d in self.depends] -        binfo.bdepends = self.depends -        binfo.bdependsigs = [d.get_ninfo() for d in self.depends if d not in ignore_set] - -        binfo.bimplicit = self.implicit or [] -        binfo.bimplicitsigs = [i.get_ninfo() for i in binfo.bimplicit if i not in ignore_set] - +        # Because self.implicit is initialized to None (and not empty list []) +        # we have to handle this case +        if not self.implicit: +            binfo.bimplicit = [] +            binfo.bimplicitsigs = [] +        else: +            binfo.bimplicit = [i for i in self.implicit if i not in ignore_set] +            binfo.bimplicitsigs = [i.get_ninfo() for i in binfo.bimplicit]          return binfo @@ -1213,7 +1241,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):          return _exists_map[self._func_exists](self)      def rexists(self): -        """Does this node exist locally or in a repositiory?""" +        """Does this node exist locally or in a repository?"""          # There are no repositories by default:          return _rexists_map[self._func_rexists](self) @@ -1452,13 +1480,18 @@ class Node(object, with_metaclass(NoSlotsPyPy)):              result = True          for child, prev_ni in zip(children, then): -            if _decider_map[child.changed_since_last_build](child, self, prev_ni): -                if t: Trace(': %s changed' % child) -                result = True +            try: +                if _decider_map[child.changed_since_last_build](child, self, prev_ni): +                    if t: Trace(': %s changed' % child) +                    result = True +            except DeciderNeedsNode as e: +                if e.decider(self, prev_ni, node=node): +                    if t: Trace(': %s changed' % child) +                    result = True -        contents = self.get_executor().get_contents()          if self.has_builder():              import SCons.Util +            contents = self.get_executor().get_contents()              newsig = SCons.Util.MD5signature(contents)              if bi.bactsig != newsig:                  if t: Trace(': bactsig %s != newsig %s' % (bi.bactsig, newsig)) @@ -1607,7 +1640,7 @@ class Node(object, with_metaclass(NoSlotsPyPy)):          # so we only print them after running them through this lambda          # to turn them into the right relative Node and then return          # its string. -        def stringify( s, E=self.dir.Entry ) : +        def stringify( s, E=self.dir.Entry):              if hasattr( s, 'dir' ) :                  return str(E(s))              return str(s) @@ -1616,15 +1649,21 @@ class Node(object, with_metaclass(NoSlotsPyPy)):          removed = [x for x in old_bkids if not x in new_bkids]          if removed: -            removed = list(map(stringify, removed)) +            removed = [stringify(r) for r in removed]              fmt = "`%s' is no longer a dependency\n"              lines.extend([fmt % s for s in removed])          for k in new_bkids:              if not k in old_bkids:                  lines.append("`%s' is a new dependency\n" % stringify(k)) -            elif _decider_map[k.changed_since_last_build](k, self, osig[k]): -                lines.append("`%s' changed\n" % stringify(k)) +            else: +                try: +                    changed = _decider_map[k.changed_since_last_build](k, self, osig[k]) +                except DeciderNeedsNode as e: +                    changed = e.decider(self, osig[k], node=self) + +                if changed: +                    lines.append("`%s' changed\n" % stringify(k))          if len(lines) == 0 and old_bkids != new_bkids:              lines.append("the dependency order changed:\n" +  | 
