diff options
Diffstat (limited to 'src')
81 files changed, 2834 insertions, 1219 deletions
diff --git a/src/Announce.txt b/src/Announce.txt index 7c6fdd5..7a13b81 100644 --- a/src/Announce.txt +++ b/src/Announce.txt @@ -18,6 +18,23 @@ So that everyone using SCons can help each other learn how to use it more effectively, please go to http://scons.org/lists.php#users to sign up for the scons-users mailing list. +==============IMPORTANT NOTICE=========== + +As has been pre-announced in SCons's mailing lists: + +* https://pairlist4.pair.net/pipermail/scons-users/2014-July/002734.html , +* https://pairlist2.pair.net/pipermail/scons-dev/2014-December/002107.html +* https://pairlist4.pair.net/pipermail/scons-users/2015-February/003454.html + +We're planning to switch the Node class to using "slots" in the core sources, +mainly to reduce memory consumption by up to 35% in large build projects. + +This feature has been tested extensively and we don't expect any problems for you. +However as with all major changes it would be wise to test V2.4.0 when it is +released. Especially if you are directly using the Node class. + +================================================================= + RELEASE VERSION/DATE TO BE FILLED IN LATER @@ -26,6 +43,39 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER since last release. This announcement highlights only the important changes. + Please note the following important changes since release 2.3.6: + - Switch several core classes to use "slots" to reduce memory + usage. (PR #2180, #2178, #2198) + + Please note the following important changes since release 2.3.5: + - Support for Visual Studio 2015 + + Please note the following important changes since release 2.3.4: + - Documentation fixes for libraries.xml and + builders-writing.xml (#2989 and #2990) + - Extended docs for InstallVersionedLib/SharedLibrary, + and added SKIP_WIN_PACKAGES argument to build script + bootstrap.py (PR #230, #3002). + - Fixed symlink support (PR #227, #2395). + - Updated debug-count test case (PR #229). + - Fixed incomplete LIBS flattening and substitution in + Program scanner(PR #205, #2954). + - Added new method rentry_exists_on_disk to Node.FS (PR #193). + - Fixed several D tests under the different OS. + - Add support for f08 file extensions for Fortran 2008 code. + - Show --config choices if no argument is specified (PR #202). + - Fixed build crash when XML toolchain isn't installed, and + activated compression for ZIP archives. + - Fix for VersionedSharedLibrary under 'sunos' platform. + - Fixed dll link with precompiled headers on MSVC 2012 + - Added an 'exclude' parameter to Glob() + - Support for multiple cmdargs (one per variant) in VS project files. + - Various improvements for TempFileMunge class. + - Added an implementation for Visual Studio users files (PR #209). + - Added support for the 'PlatformToolset' tag in VS project files (#2978). + - Added support for '-isystem' to ParseFlags. + + Please note the following important changes since release 2.3.3: -- Fix for EnsureSConsVersion regression in 2.3.3. diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 2a2c21f..11fdeba 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -6,6 +6,62 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER + From Florian Miedniak: + - Fixed tigris issue #3011: Glob() excludes didn't work when used with VariantDir(duplicate=0) + + From William Blevins: + - InstallVersionedLib now available in the DefaultEnvironment context. + - Improves orthogonality of use cases between different Install functions. + + From William Roberts: + - Fix bug 2831 and allow Help() text to be appended to AddOption() help. + + From Paweł Tomulik: + - Reimplemented versioning for shared libraries, with the following effects + - Fixed tigris issues #3001, #3006. + - Fixed several other issues not reported to tigris, including: + issues with versioned libraries in subdirectories with tricky names, + issues with versioned libraries and variant directories, + issue with soname not being injected to library when using D linkers, + - Switched to direct symlinks instead of daisy-chained ones -- soname and + development symlinks point directly to the versioned shared library now), + for rationale see: + https://www.debian.org/doc/debian-policy/ch-sharedlibs.html + https://fedoraproject.org/wiki/Packaging:Guidelines#Devel_Packages + https://bitbucket.org/scons/scons/pull-requests/247/new-versioned-libraries-gnulink-cyglink/diff#comment-10063929 + - New construction variables to allow override default behavior: SONAME, + SHLIBVERSIONFLAGS, _SHLIBVERSIONFLAGS, SHLIBNOVERSIONSYMLINKS, + LDMODULEVERSION, LDMODULEVERSIONFLAGS, _LDMODULEVERSIONFLAGS, + LDMODULENOVERSIONSYMLINKS. + - Changed logic used to configure the versioning machinery from + platform-centric to linker-oriented. + - The SHLIBVERSION/LDMODULEVERSION variables are no longer validated by + SCons (more freedom to users). + - InstallVersionedLib() doesn't use SHLIBVERSION anymore. + - Enchanced docs for the library versioning stuff. + - New tests for versioned libraries. + - Library versioning is currently implemented for the following linker + tools: 'cyglink', 'gnulink', 'sunlink'. + +RELEASE 2.4.0 - Mon, 21 Sep 2015 08:56:00 -0700 + + From Dirk Baechle: + - Switched several core classes to use "slots", to + reduce the overall memory consumption in large + projects (fixes #2180, #2178, #2198) + - Memoizer counting uses decorators now, instead of + the old metaclasses approach. + + From Andrew Featherstone + - Fixed typo in SWIGPATH description + +RELEASE 2.3.6 - Mon, 31 Jul 2015 14:35:03 -0700 + + From Rob Smith: + - Added support for Visual Studio 2015 + +RELEASE 2.3.5 - Mon, 17 Jun 2015 21:07:32 -0700 + From Stephen Pollard: - Documentation fixes for libraries.xml and builders-writing.xml (#2989 and #2990) @@ -36,8 +92,6 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER - Show --config choices if no argument is specified (PR #202). - Fixed build crash when XML toolchain isn't installed, and activated compression for ZIP archives. - - Fixed --tree=all print when build tree contains non-ascii - Node representation (PR #235). From Alexandre Feblot: - Fix for VersionedSharedLibrary under 'sunos' platform. diff --git a/src/README.txt b/src/README.txt index 5d880c2..d80460a 100644 --- a/src/README.txt +++ b/src/README.txt @@ -28,7 +28,8 @@ the latest version by checking the SCons download page at: EXECUTION REQUIREMENTS ====================== -Running SCons requires Python version 2.4 or later. There should be +Running SCons requires Python version 2.7.*. Currently it does not +run on the Python 3.x release. There should be no other dependencies or requirements to run SCons. (There is, however, an additional requirement to *install* SCons from this particular package; see the next section.) @@ -224,20 +225,26 @@ Check the SCons web site at: AUTHOR INFO =========== - -Steven Knight -knight at baldmt dot com -http://www.baldmt.com/~knight/ - -With plenty of help from the SCons Development team: - Chad Austin - Charles Crain - Steve Leblanc - Greg Noel - Gary Oberbrunner - Anthony Roach - Greg Spencer - Christoph Wiedemann - -__COPYRIGHT__ -__FILE__ __REVISION__ __DATE__ __DEVELOPER__ +SCons was originally written by Steven Knight, knight at baldmt dot com. +Since around 2010 it has been maintained by the SCons +development team, co-managed by Bill Deegan and Gary Oberbrunner, with +many contributors, including but not at all limited to: + +- Chad Austin +- Dirk Baechle +- Charles Crain +- William Deegan +- Steve Leblanc +- Rob Managan +- Greg Noel +- Gary Oberbrunner +- Anthony Roach +- Greg Spencer +- Tom Tanner +- Anatoly Techtonik +- Christoph Wiedemann +- Russel Winder + +\... and many others. + +Copyright (c) 2001 - 2015 The SCons Foundation diff --git a/src/RELEASE.txt b/src/RELEASE.txt index 1fa033b..1bf2eb1 100644 --- a/src/RELEASE.txt +++ b/src/RELEASE.txt @@ -1,4 +1,4 @@ - A new SCons checkpoint release, 2.3.5.alpha.yyyymmdd, is now available + A new SCons checkpoint release, 2.3.13.alpha.yyyymmdd, is now available on the SCons download page: http://www.scons.org/download.php diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index eecea11..5a34825 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -540,7 +540,7 @@ class _ActionAction(ActionBase): if chdir: save_cwd = os.getcwd() try: - chdir = str(chdir.abspath) + chdir = str(chdir.get_abspath()) except AttributeError: if not is_String(chdir): if executor: diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py index c82f6ac..769b15d 100644 --- a/src/engine/SCons/Builder.py +++ b/src/engine/SCons/Builder.py @@ -352,11 +352,6 @@ class BuilderBase(object): nodes (files) from input nodes (files). """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] - def __init__(self, action = None, prefix = '', suffix = '', @@ -758,8 +753,7 @@ class BuilderBase(object): def _get_src_builders_key(self, env): return id(env) - memoizer_counters.append(SCons.Memoize.CountDict('get_src_builders', _get_src_builders_key)) - + @SCons.Memoize.CountDictCall(_get_src_builders_key) def get_src_builders(self, env): """ Returns the list of source Builders for this Builder. @@ -795,8 +789,7 @@ class BuilderBase(object): def _subst_src_suffixes_key(self, env): return id(env) - memoizer_counters.append(SCons.Memoize.CountDict('subst_src_suffixes', _subst_src_suffixes_key)) - + @SCons.Memoize.CountDictCall(_subst_src_suffixes_key) def subst_src_suffixes(self, env): """ The suffix list may contain construction variable expansions, diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py index 70a7a3f..3eca588 100644 --- a/src/engine/SCons/BuilderTests.py +++ b/src/engine/SCons/BuilderTests.py @@ -163,7 +163,8 @@ class MyNode_without_target_from_source(object): self.builder = None self.is_explicit = None self.side_effect = 0 - self.suffix = os.path.splitext(name)[1] + def get_suffix(self): + return os.path.splitext(self.name)[1] def disambiguate(self): return self def __str__(self): @@ -349,7 +350,7 @@ class BuilderTestCase(unittest.TestCase): builder = SCons.Builder.Builder(action="foo") target = builder(env, None, source='n22', srcdir='src_dir')[0] - p = target.sources[0].path + p = target.sources[0].get_internal_path() assert p == os.path.join('src_dir', 'n22'), p def test_mistaken_variables(self): @@ -487,20 +488,20 @@ class BuilderTestCase(unittest.TestCase): builder = SCons.Builder.Builder(prefix = 'lib', action='') assert builder.get_prefix(env) == 'lib' tgt = builder(env, target = 'tgt1', source = 'src1')[0] - assert tgt.path == 'libtgt1', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'libtgt1', \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = 'tgt2a tgt2b', source = 'src2')[0] - assert tgt.path == 'libtgt2a tgt2b', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'libtgt2a tgt2b', \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = None, source = 'src3')[0] - assert tgt.path == 'libsrc3', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'libsrc3', \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = None, source = 'lib/src4')[0] - assert tgt.path == os.path.join('lib', 'libsrc4'), \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == os.path.join('lib', 'libsrc4'), \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = 'lib/tgt5', source = 'lib/src5')[0] - assert tgt.path == os.path.join('lib', 'libtgt5'), \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == os.path.join('lib', 'libtgt5'), \ + "Target has unexpected name: %s" % tgt.get_internal_path() def gen_prefix(env, sources): return "gen_prefix() says " + env['FOO'] @@ -520,17 +521,17 @@ class BuilderTestCase(unittest.TestCase): '.zzz' : my_emit}, action = '') tgt = builder(my_env, target = None, source = 'f1')[0] - assert tgt.path == 'default-f1', tgt.path + assert tgt.get_internal_path() == 'default-f1', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f2.c')[0] - assert tgt.path == 'default-f2', tgt.path + assert tgt.get_internal_path() == 'default-f2', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f3.in')[0] - assert tgt.path == 'out-f3', tgt.path + assert tgt.get_internal_path() == 'out-f3', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f4.x')[0] - assert tgt.path == 'y-f4', tgt.path + assert tgt.get_internal_path() == 'y-f4', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f5.foo')[0] - assert tgt.path == 'foo-f5', tgt.path + assert tgt.get_internal_path() == 'foo-f5', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f6.zzz')[0] - assert tgt.path == 'emit-f6', tgt.path + assert tgt.get_internal_path() == 'emit-f6', tgt.get_internal_path() def test_set_suffix(self): """Test the set_suffix() method""" @@ -560,13 +561,13 @@ class BuilderTestCase(unittest.TestCase): assert b1.src_suffixes(env) == ['.c'], b1.src_suffixes(env) tgt = b1(env, target = 'tgt2', source = 'src2')[0] - assert tgt.sources[0].path == 'src2.c', \ - "Source has unexpected name: %s" % tgt.sources[0].path + assert tgt.sources[0].get_internal_path() == 'src2.c', \ + "Source has unexpected name: %s" % tgt.sources[0].get_internal_path() tgt = b1(env, target = 'tgt3', source = 'src3a src3b')[0] assert len(tgt.sources) == 1 - assert tgt.sources[0].path == 'src3a src3b.c', \ - "Unexpected tgt.sources[0] name: %s" % tgt.sources[0].path + assert tgt.sources[0].get_internal_path() == 'src3a src3b.c', \ + "Unexpected tgt.sources[0] name: %s" % tgt.sources[0].get_internal_path() b2 = SCons.Builder.Builder(src_suffix = '.2', src_builder = b1) r = sorted(b2.src_suffixes(env)) @@ -636,14 +637,14 @@ class BuilderTestCase(unittest.TestCase): builder = SCons.Builder.Builder(suffix = 'o', action='') assert builder.get_suffix(env) == '.o', builder.get_suffix(env) tgt = builder(env, target = 'tgt3', source = 'src3')[0] - assert tgt.path == 'tgt3.o', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'tgt3.o', \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = 'tgt4a tgt4b', source = 'src4')[0] - assert tgt.path == 'tgt4a tgt4b.o', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'tgt4a tgt4b.o', \ + "Target has unexpected name: %s" % tgt.get_internal_path() tgt = builder(env, target = None, source = 'src5')[0] - assert tgt.path == 'src5.o', \ - "Target has unexpected name: %s" % tgt.path + assert tgt.get_internal_path() == 'src5.o', \ + "Target has unexpected name: %s" % tgt.get_internal_path() def gen_suffix(env, sources): return "gen_suffix() says " + env['BAR'] @@ -663,17 +664,17 @@ class BuilderTestCase(unittest.TestCase): '.zzz' : my_emit}, action='') tgt = builder(my_env, target = None, source = 'f1')[0] - assert tgt.path == 'f1.default', tgt.path + assert tgt.get_internal_path() == 'f1.default', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f2.c')[0] - assert tgt.path == 'f2.default', tgt.path + assert tgt.get_internal_path() == 'f2.default', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f3.in')[0] - assert tgt.path == 'f3.out', tgt.path + assert tgt.get_internal_path() == 'f3.out', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f4.x')[0] - assert tgt.path == 'f4.y', tgt.path + assert tgt.get_internal_path() == 'f4.y', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f5.bar')[0] - assert tgt.path == 'f5.new', tgt.path + assert tgt.get_internal_path() == 'f5.new', tgt.get_internal_path() tgt = builder(my_env, target = None, source = 'f6.zzz')[0] - assert tgt.path == 'f6.emit', tgt.path + assert tgt.get_internal_path() == 'f6.emit', tgt.get_internal_path() def test_single_source(self): """Test Builder with single_source flag set""" diff --git a/src/engine/SCons/CacheDir.py b/src/engine/SCons/CacheDir.py index 9dd18e5..f32b326 100644 --- a/src/engine/SCons/CacheDir.py +++ b/src/engine/SCons/CacheDir.py @@ -50,11 +50,11 @@ def CacheRetrieveFunc(target, source, env): cd.CacheDebug('CacheRetrieve(%s): retrieving from %s\n', t, cachefile) if SCons.Action.execute_actions: if fs.islink(cachefile): - fs.symlink(fs.readlink(cachefile), t.path) + fs.symlink(fs.readlink(cachefile), t.get_internal_path()) else: - env.copy_from_cache(cachefile, t.path) + env.copy_from_cache(cachefile, t.get_internal_path()) st = fs.stat(cachefile) - fs.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) + fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) return 0 def CacheRetrieveString(target, source, env): @@ -63,7 +63,7 @@ def CacheRetrieveString(target, source, env): cd = env.get_CacheDir() cachedir, cachefile = cd.cachepath(t) if t.fs.exists(cachefile): - return "Retrieved `%s' from cache" % t.path + return "Retrieved `%s' from cache" % t.get_internal_path() return None CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString) @@ -106,12 +106,12 @@ def CachePushFunc(target, source, env): raise SCons.Errors.EnvironmentError(msg) try: - if fs.islink(t.path): - fs.symlink(fs.readlink(t.path), tempfile) + if fs.islink(t.get_internal_path()): + fs.symlink(fs.readlink(t.get_internal_path()), tempfile) else: - fs.copy2(t.path, tempfile) + fs.copy2(t.get_internal_path(), tempfile) fs.rename(tempfile, cachefile) - st = fs.stat(t.path) + st = fs.stat(t.get_internal_path()) fs.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) except EnvironmentError: # It's possible someone else tried writing the file at the diff --git a/src/engine/SCons/Debug.py b/src/engine/SCons/Debug.py index 9974039..b47c24c 100644 --- a/src/engine/SCons/Debug.py +++ b/src/engine/SCons/Debug.py @@ -34,6 +34,7 @@ import os import sys import time import weakref +import inspect # Global variable that gets set to 'True' by the Main script, # when the creation of class instances should get tracked. @@ -46,7 +47,12 @@ def logInstanceCreation(instance, name=None): name = instance.__class__.__name__ if name not in tracked_classes: tracked_classes[name] = [] - tracked_classes[name].append(weakref.ref(instance)) + if hasattr(instance, '__dict__'): + tracked_classes[name].append(weakref.ref(instance)) + else: + # weakref doesn't seem to work when the instance + # contains only slots... + tracked_classes[name].append(instance) def string_to_classes(s): if s == '*': @@ -66,7 +72,10 @@ def listLoggedInstances(classes, file=sys.stdout): for classname in string_to_classes(classes): file.write('\n%s:\n' % classname) for ref in tracked_classes[classname]: - obj = ref() + if inspect.isclass(ref): + obj = ref() + else: + obj = ref if obj is not None: file.write(' %s\n' % repr(obj)) diff --git a/src/engine/SCons/Defaults.py b/src/engine/SCons/Defaults.py index 6500443..744da5f 100644 --- a/src/engine/SCons/Defaults.py +++ b/src/engine/SCons/Defaults.py @@ -482,6 +482,15 @@ class Variable_Method_Caller(object): frame = frame.f_back return None +# if env[version_var] id defined, returns env[flags_var], otherwise returns None +def __libversionflags(env, version_var, flags_var): + try: + if env[version_var]: + return env[flags_var] + except KeyError: + pass + return None + ConstructionEnvironment = { 'BUILDERS' : {}, 'SCANNERS' : [], @@ -499,6 +508,12 @@ ConstructionEnvironment = { '_LIBDIRFLAGS' : '$( ${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, RDirs, TARGET, SOURCE)} $)', '_CPPINCFLAGS' : '$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)} $)', '_CPPDEFFLAGS' : '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__)}', + + '__libversionflags' : __libversionflags, + '__SHLIBVERSIONFLAGS' : '${__libversionflags(__env__,"SHLIBVERSION","_SHLIBVERSIONFLAGS")}', + '__LDMODULEVERSIONFLAGS' : '${__libversionflags(__env__,"LDMODULEVERSION","_LDMODULEVERSIONFLAGS")}', + '__DSHLIBVERSIONFLAGS' : '${__libversionflags(__env__,"DSHLIBVERSION","_DSHLIBVERSIONFLAGS")}', + 'TEMPFILE' : NullCmdGenerator, 'Dir' : Variable_Method_Caller('TARGET', 'Dir'), 'Dirs' : Variable_Method_Caller('TARGET', 'Dirs'), diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index 6886e85..7e2f896 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -365,9 +365,6 @@ class SubstitutionEnvironment(object): class actually becomes useful.) """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - def __init__(self, **kw): """Initialization of an underlying SubstitutionEnvironment class. """ @@ -615,7 +612,7 @@ class SubstitutionEnvironment(object): def Override(self, overrides): """ - Produce a modified environment whose variables are overriden by + Produce a modified environment whose variables are overridden by the overrides dictionaries. "overrides" is a dictionary that will override the variables of this environment. @@ -902,8 +899,6 @@ class Base(SubstitutionEnvironment): Environment. """ - memoizer_counters = [] - ####################################################################### # This is THE class for interacting with the SCons build engine, # and it contains a lot of stuff, so we're going to try to keep this @@ -1071,8 +1066,7 @@ class Base(SubstitutionEnvironment): factory = getattr(self.fs, name) return factory - memoizer_counters.append(SCons.Memoize.CountValue('_gsm')) - + @SCons.Memoize.CountMethodCall def _gsm(self): try: return self._memo['_gsm'] @@ -1528,8 +1522,8 @@ class Base(SubstitutionEnvironment): def Dump(self, key = None): """ - Using the standard Python pretty printer, dump the contents of the - scons build environment to stdout. + Using the standard Python pretty printer, return the contents of the + scons build environment as a string. If the key passed in is anything other than None, then that will be used as an index into the build environment dictionary and @@ -1802,7 +1796,7 @@ class Base(SubstitutionEnvironment): self.Replace(**kw) def _find_toolpath_dir(self, tp): - return self.fs.Dir(self.subst(tp)).srcnode().abspath + return self.fs.Dir(self.subst(tp)).srcnode().get_abspath() def Tool(self, tool, toolpath=None, **kw): if SCons.Util.is_String(tool): diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py index a0869e8..9a9d2b0 100644 --- a/src/engine/SCons/EnvironmentTests.py +++ b/src/engine/SCons/EnvironmentTests.py @@ -2703,25 +2703,25 @@ def generate(env): t = env.AlwaysBuild('a', 'b$FOO', ['c', 'd'], '$BAR', env.fs.Dir('dir'), env.fs.File('file')) assert t[0].__class__.__name__ == 'Entry' - assert t[0].path == 'a' + assert t[0].get_internal_path() == 'a' assert t[0].always_build assert t[1].__class__.__name__ == 'Entry' - assert t[1].path == 'bfff' + assert t[1].get_internal_path() == 'bfff' assert t[1].always_build assert t[2].__class__.__name__ == 'Entry' - assert t[2].path == 'c' + assert t[2].get_internal_path() == 'c' assert t[2].always_build assert t[3].__class__.__name__ == 'Entry' - assert t[3].path == 'd' + assert t[3].get_internal_path() == 'd' assert t[3].always_build assert t[4].__class__.__name__ == 'Entry' - assert t[4].path == 'bbb' + assert t[4].get_internal_path() == 'bbb' assert t[4].always_build assert t[5].__class__.__name__ == 'Dir' - assert t[5].path == 'dir' + assert t[5].get_internal_path() == 'dir' assert t[5].always_build assert t[6].__class__.__name__ == 'File' - assert t[6].path == 'file' + assert t[6].get_internal_path() == 'file' assert t[6].always_build def test_VariantDir(self): @@ -2811,13 +2811,13 @@ def generate(env): assert t.builder is not None assert t.builder.action.__class__.__name__ == 'CommandAction' assert t.builder.action.cmd_list == 'buildfoo $target $source' - assert 'foo1.in' in [x.path for x in t.sources] - assert 'foo2.in' in [x.path for x in t.sources] + assert 'foo1.in' in [x.get_internal_path() for x in t.sources] + assert 'foo2.in' in [x.get_internal_path() for x in t.sources] sub = env.fs.Dir('sub') t = env.Command(target='bar.out', source='sub', action='buildbar $target $source')[0] - assert 'sub' in [x.path for x in t.sources] + assert 'sub' in [x.get_internal_path() for x in t.sources] def testFunc(env, target, source): assert str(target[0]) == 'foo.out' @@ -2828,8 +2828,8 @@ def generate(env): assert t.builder is not None assert t.builder.action.__class__.__name__ == 'FunctionAction' t.build() - assert 'foo1.in' in [x.path for x in t.sources] - assert 'foo2.in' in [x.path for x in t.sources] + assert 'foo1.in' in [x.get_internal_path() for x in t.sources] + assert 'foo2.in' in [x.get_internal_path() for x in t.sources] x = [] def test2(baz, x=x): @@ -2846,7 +2846,7 @@ def generate(env): action = 'foo', X = 'xxx')[0] assert str(t) == 'xxx.out', str(t) - assert 'xxx.in' in [x.path for x in t.sources] + assert 'xxx.in' in [x.get_internal_path() for x in t.sources] env = self.TestEnvironment(source_scanner = 'should_not_find_this') t = env.Command(target='file.out', source='file.in', @@ -2890,27 +2890,27 @@ def generate(env): t = env.Depends(target='EnvironmentTest.py', dependency='Environment.py')[0] assert t.__class__.__name__ == 'Entry', t.__class__.__name__ - assert t.path == 'EnvironmentTest.py' + assert t.get_internal_path() == 'EnvironmentTest.py' assert len(t.depends) == 1 d = t.depends[0] assert d.__class__.__name__ == 'Entry', d.__class__.__name__ - assert d.path == 'Environment.py' + assert d.get_internal_path() == 'Environment.py' t = env.Depends(target='${FOO}.py', dependency='${BAR}.py')[0] assert t.__class__.__name__ == 'File', t.__class__.__name__ - assert t.path == 'xxx.py' + assert t.get_internal_path() == 'xxx.py' assert len(t.depends) == 1 d = t.depends[0] assert d.__class__.__name__ == 'File', d.__class__.__name__ - assert d.path == 'yyy.py' + assert d.get_internal_path() == 'yyy.py' t = env.Depends(target='dir1', dependency='dir2')[0] assert t.__class__.__name__ == 'Dir', t.__class__.__name__ - assert t.path == 'dir1' + assert t.get_internal_path() == 'dir1' assert len(t.depends) == 1 d = t.depends[0] assert d.__class__.__name__ == 'Dir', d.__class__.__name__ - assert d.path == 'dir2' + assert d.get_internal_path() == 'dir2' def test_Dir(self): """Test the Dir() method""" @@ -2944,19 +2944,19 @@ def generate(env): t = env.NoClean('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO') assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__ - assert t[0].path == 'p_a' + assert t[0].get_internal_path() == 'p_a' assert t[0].noclean assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__ - assert t[1].path == 'p_hhhb' + assert t[1].get_internal_path() == 'p_hhhb' assert t[1].noclean assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__ - assert t[2].path == 'p_c' + assert t[2].get_internal_path() == 'p_c' assert t[2].noclean assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__ - assert t[3].path == 'p_d' + assert t[3].get_internal_path() == 'p_d' assert t[3].noclean assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__ - assert t[4].path == 'p_ggg' + assert t[4].get_internal_path() == 'p_ggg' assert t[4].noclean def test_Dump(self): @@ -3074,27 +3074,27 @@ def generate(env): t = env.Ignore(target='targ.py', dependency='dep.py')[0] assert t.__class__.__name__ == 'Entry', t.__class__.__name__ - assert t.path == 'targ.py' + assert t.get_internal_path() == 'targ.py' assert len(t.ignore) == 1 i = t.ignore[0] assert i.__class__.__name__ == 'Entry', i.__class__.__name__ - assert i.path == 'dep.py' + assert i.get_internal_path() == 'dep.py' t = env.Ignore(target='$FOO$BAR', dependency='$BAR$FOO')[0] assert t.__class__.__name__ == 'File', t.__class__.__name__ - assert t.path == 'yyyzzz' + assert t.get_internal_path() == 'yyyzzz' assert len(t.ignore) == 1 i = t.ignore[0] assert i.__class__.__name__ == 'File', i.__class__.__name__ - assert i.path == 'zzzyyy' + assert i.get_internal_path() == 'zzzyyy' t = env.Ignore(target='dir1', dependency='dir2')[0] assert t.__class__.__name__ == 'Dir', t.__class__.__name__ - assert t.path == 'dir1' + assert t.get_internal_path() == 'dir1' assert len(t.ignore) == 1 i = t.ignore[0] assert i.__class__.__name__ == 'Dir', i.__class__.__name__ - assert i.path == 'dir2' + assert i.get_internal_path() == 'dir2' def test_Literal(self): """Test the Literal() method""" @@ -3123,19 +3123,19 @@ def generate(env): t = env.Precious('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO') assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__ - assert t[0].path == 'p_a' + assert t[0].get_internal_path() == 'p_a' assert t[0].precious assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__ - assert t[1].path == 'p_hhhb' + assert t[1].get_internal_path() == 'p_hhhb' assert t[1].precious assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__ - assert t[2].path == 'p_c' + assert t[2].get_internal_path() == 'p_c' assert t[2].precious assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__ - assert t[3].path == 'p_d' + assert t[3].get_internal_path() == 'p_d' assert t[3].precious assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__ - assert t[4].path == 'p_ggg' + assert t[4].get_internal_path() == 'p_ggg' assert t[4].precious def test_Pseudo(self): @@ -3146,19 +3146,19 @@ def generate(env): t = env.Pseudo('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO') assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__ - assert t[0].path == 'p_a' + assert t[0].get_internal_path() == 'p_a' assert t[0].pseudo assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__ - assert t[1].path == 'p_hhhb' + assert t[1].get_internal_path() == 'p_hhhb' assert t[1].pseudo assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__ - assert t[2].path == 'p_c' + assert t[2].get_internal_path() == 'p_c' assert t[2].pseudo assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__ - assert t[3].path == 'p_d' + assert t[3].get_internal_path() == 'p_d' assert t[3].pseudo assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__ - assert t[4].path == 'p_ggg' + assert t[4].get_internal_path() == 'p_ggg' assert t[4].pseudo def test_Repository(self): @@ -3263,7 +3263,7 @@ def generate(env): bar = env.Object('bar.obj', 'bar.cpp')[0] s = env.SideEffect('mylib.pdb', ['foo.obj', 'bar.obj'])[0] assert s.__class__.__name__ == 'Entry', s.__class__.__name__ - assert s.path == 'mylib.pdb' + assert s.get_internal_path() == 'mylib.pdb' assert s.side_effect assert foo.side_effects == [s] assert bar.side_effects == [s] @@ -3272,7 +3272,7 @@ def generate(env): bbb = env.Object('bbb.obj', 'bbb.cpp')[0] s = env.SideEffect('my${LIB}.pdb', ['${FOO}.obj', '${BAR}.obj'])[0] assert s.__class__.__name__ == 'File', s.__class__.__name__ - assert s.path == 'mylll.pdb' + assert s.get_internal_path() == 'mylll.pdb' assert s.side_effect assert fff.side_effects == [s], fff.side_effects assert bbb.side_effects == [s], bbb.side_effects @@ -3281,7 +3281,7 @@ def generate(env): ccc = env.Object('ccc.obj', 'ccc.cpp')[0] s = env.SideEffect('mymmm.pdb', ['ggg.obj', 'ccc.obj'])[0] assert s.__class__.__name__ == 'Dir', s.__class__.__name__ - assert s.path == 'mymmm.pdb' + assert s.get_internal_path() == 'mymmm.pdb' assert s.side_effect assert ggg.side_effects == [s], ggg.side_effects assert ccc.side_effects == [s], ccc.side_effects @@ -3290,18 +3290,18 @@ def generate(env): """Test the SourceCode() method.""" env = self.TestEnvironment(FOO='mmm', BAR='nnn') e = env.SourceCode('foo', None)[0] - assert e.path == 'foo' + assert e.get_internal_path() == 'foo' s = e.src_builder() assert s is None, s b = Builder() e = env.SourceCode(e, b)[0] - assert e.path == 'foo' + assert e.get_internal_path() == 'foo' s = e.src_builder() assert s is b, s e = env.SourceCode('$BAR$FOO', None)[0] - assert e.path == 'nnnmmm' + assert e.get_internal_path() == 'nnnmmm' s = e.src_builder() assert s is None, s diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py index 388f8ac..98ed758 100644 --- a/src/engine/SCons/Executor.py +++ b/src/engine/SCons/Executor.py @@ -40,6 +40,10 @@ import SCons.Memoize class Batch(object): """Remembers exact association between targets and sources of executor.""" + + __slots__ = ('targets', + 'sources') + def __init__(self, targets=[], sources=[]): self.targets = targets self.sources = sources @@ -109,6 +113,48 @@ def rfile(node): return rfile() +def execute_nothing(obj, target, kw): + return 0 + +def execute_action_list(obj, target, kw): + """Actually execute the action list.""" + env = obj.get_build_env() + kw = obj.get_kw(kw) + status = 0 + for act in obj.get_action_list(): + #args = (self.get_all_targets(), self.get_all_sources(), env) + args = ([], [], env) + status = act(*args, **kw) + if isinstance(status, SCons.Errors.BuildError): + status.executor = obj + raise status + elif status: + msg = "Error %s" % status + raise SCons.Errors.BuildError( + errstr=msg, + node=obj.batches[0].targets, + executor=obj, + action=act) + return status + +_do_execute_map = {0 : execute_nothing, + 1 : execute_action_list} + + +def execute_actions_str(obj): + env = obj.get_build_env() + return "\n".join([action.genstring(obj.get_all_targets(), + obj.get_all_sources(), + env) + for action in obj.get_action_list()]) + +def execute_null_str(obj): + return '' + +_execute_str_map = {0 : execute_null_str, + 1 : execute_actions_str} + + class Executor(object): """A class for controlling instances of executing an action. @@ -117,10 +163,21 @@ class Executor(object): and sources for later processing as needed. """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] + __slots__ = ('pre_actions', + 'post_actions', + 'env', + 'overridelist', + 'batches', + 'builder_kw', + '_memo', + 'lvars', + '_changed_sources_list', + '_changed_targets_list', + '_unchanged_sources_list', + '_unchanged_targets_list', + 'action_list', + '_do_execute', + '_execute_str') def __init__(self, action, env=None, overridelist=[{}], targets=[], sources=[], builder_kw={}): @@ -135,6 +192,8 @@ class Executor(object): else: self.batches = [] self.builder_kw = builder_kw + self._do_execute = 1 + self._execute_str = 1 self._memo = {} def get_lvars(self): @@ -284,8 +343,7 @@ class Executor(object): result.extend(target.side_effects) return result - memoizer_counters.append(SCons.Memoize.CountValue('get_build_env')) - + @SCons.Memoize.CountMethodCall def get_build_env(self): """Fetch or create the appropriate build Environment for this Executor. @@ -330,36 +388,12 @@ class Executor(object): result['executor'] = self return result - def do_nothing(self, target, kw): - return 0 - - def do_execute(self, target, kw): - """Actually execute the action list.""" - env = self.get_build_env() - kw = self.get_kw(kw) - status = 0 - for act in self.get_action_list(): - #args = (self.get_all_targets(), self.get_all_sources(), env) - args = ([], [], env) - status = act(*args, **kw) - if isinstance(status, SCons.Errors.BuildError): - status.executor = self - raise status - elif status: - msg = "Error %s" % status - raise SCons.Errors.BuildError( - errstr=msg, - node=self.batches[0].targets, - executor=self, - action=act) - return status - # use extra indirection because with new-style objects (Python 2.2 # and above) we can't override special methods, and nullify() needs # to be able to do this. def __call__(self, target, **kw): - return self.do_execute(target, kw) + return _do_execute_map[self._do_execute](self, target, kw) def cleanup(self): self._memo = {} @@ -403,24 +437,15 @@ class Executor(object): # another extra indirection for new-style objects and nullify... - def my_str(self): - env = self.get_build_env() - return "\n".join([action.genstring(self.get_all_targets(), - self.get_all_sources(), - env) - for action in self.get_action_list()]) - - def __str__(self): - return self.my_str() + return _execute_str_map[self._execute_str](self) def nullify(self): self.cleanup() - self.do_execute = self.do_nothing - self.my_str = lambda: '' - - memoizer_counters.append(SCons.Memoize.CountValue('get_contents')) + self._do_execute = 0 + self._execute_str = 0 + @SCons.Memoize.CountMethodCall def get_contents(self): """Fetch the signature contents. This is the main reason this class exists, so we can compute this once and cache it regardless @@ -493,8 +518,7 @@ class Executor(object): def _get_unignored_sources_key(self, node, ignore=()): return (node,) + tuple(ignore) - memoizer_counters.append(SCons.Memoize.CountDict('get_unignored_sources', _get_unignored_sources_key)) - + @SCons.Memoize.CountDictCall(_get_unignored_sources_key) def get_unignored_sources(self, node, ignore=()): key = (node,) + tuple(ignore) try: @@ -579,6 +603,23 @@ class Null(object): disassociate Builders from Nodes entirely, so we're not going to worry about unit tests for this--at least for now. """ + + __slots__ = ('pre_actions', + 'post_actions', + 'env', + 'overridelist', + 'batches', + 'builder_kw', + '_memo', + 'lvars', + '_changed_sources_list', + '_changed_targets_list', + '_unchanged_sources_list', + '_unchanged_targets_list', + 'action_list', + '_do_execute', + '_execute_str') + def __init__(self, *args, **kw): if SCons.Debug.track_instances: logInstanceCreation(self, 'Executor.Null') self.batches = [Batch(kw['targets'][:], [])] diff --git a/src/engine/SCons/Memoize.py b/src/engine/SCons/Memoize.py index e77aacf..5144f83 100644 --- a/src/engine/SCons/Memoize.py +++ b/src/engine/SCons/Memoize.py @@ -25,17 +25,17 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" __doc__ = """Memoizer -A metaclass implementation to count hits and misses of the computed +A decorator-based implementation to count hits and misses of the computed values that various methods cache in memory. Use of this modules assumes that wrapped methods be coded to cache their -values in a consistent way. Here is an example of wrapping a method -that returns a computed value, with no input parameters: +values in a consistent way. In particular, it requires that the class uses a +dictionary named "_memo" to store the cached values. - memoizer_counters = [] # Memoization - - memoizer_counters.append(SCons.Memoize.CountValue('foo')) # Memoization +Here is an example of wrapping a method that returns a computed value, +with no input parameters: + @SCons.Memoize.CountMethodCall def foo(self): try: # Memoization @@ -55,8 +55,7 @@ based on one or more input arguments: def _bar_key(self, argument): # Memoization return argument # Memoization - memoizer_counters.append(SCons.Memoize.CountDict('bar', _bar_key)) # Memoization - + @SCons.Memoize.CountDictCall(_bar_key) def bar(self, argument): memo_key = argument # Memoization @@ -77,10 +76,6 @@ based on one or more input arguments: return result -At one point we avoided replicating this sort of logic in all the methods -by putting it right into this module, but we've moved away from that at -present (see the "Historical Note," below.). - Deciding what to cache is tricky, because different configurations can have radically different performance tradeoffs, and because the tradeoffs involved are often so non-obvious. Consequently, deciding @@ -102,51 +97,37 @@ cache return values from a method that's being called a lot: input arguments, you don't need to use all of the arguments if some of them don't affect the return values. -Historical Note: The initial Memoizer implementation actually handled -the caching of values for the wrapped methods, based on a set of generic -algorithms for computing hashable values based on the method's arguments. -This collected caching logic nicely, but had two drawbacks: - - Running arguments through a generic key-conversion mechanism is slower - (and less flexible) than just coding these things directly. Since the - methods that need memoized values are generally performance-critical, - slowing them down in order to collect the logic isn't the right - tradeoff. - - Use of the memoizer really obscured what was being called, because - all the memoized methods were wrapped with re-used generic methods. - This made it more difficult, for example, to use the Python profiler - to figure out how to optimize the underlying methods. """ -import types - # A flag controlling whether or not we actually use memoization. use_memoizer = None -CounterList = [] +# Global list of counter objects +CounterList = {} class Counter(object): """ Base class for counting memoization hits and misses. - We expect that the metaclass initialization will have filled in - the .name attribute that represents the name of the function - being counted. + We expect that the initialization in a matching decorator will + fill in the correct class name and method name that represents + the name of the function being counted. """ - def __init__(self, method_name): + def __init__(self, cls_name, method_name): """ """ + self.cls_name = cls_name self.method_name = method_name self.hit = 0 self.miss = 0 - CounterList.append(self) + def key(self): + return self.cls_name+'.'+self.method_name def display(self): fmt = " %7d hits %7d misses %s()" - print fmt % (self.hit, self.miss, self.name) + print fmt % (self.hit, self.miss, self.key()) def __cmp__(self, other): try: - return cmp(self.name, other.name) + return cmp(self.key(), other.key()) except AttributeError: return 0 @@ -154,45 +135,39 @@ class CountValue(Counter): """ A counter class for simple, atomic memoized values. - A CountValue object should be instantiated in a class for each of + A CountValue object should be instantiated in a decorator for each of the class's methods that memoizes its return value by simply storing the return value in its _memo dictionary. - - We expect that the metaclass initialization will fill in the - .underlying_method attribute with the method that we're wrapping. - We then call the underlying_method method after counting whether - its memoized value has already been set (a hit) or not (a miss). """ - def __call__(self, *args, **kw): + def count(self, *args, **kw): + """ Counts whether the memoized value has already been + set (a hit) or not (a miss). + """ obj = args[0] if self.method_name in obj._memo: self.hit = self.hit + 1 else: self.miss = self.miss + 1 - return self.underlying_method(*args, **kw) class CountDict(Counter): """ A counter class for memoized values stored in a dictionary, with keys based on the method's input arguments. - A CountDict object is instantiated in a class for each of the + A CountDict object is instantiated in a decorator for each of the class's methods that memoizes its return value in a dictionary, indexed by some key that can be computed from one or more of its input arguments. - - We expect that the metaclass initialization will fill in the - .underlying_method attribute with the method that we're wrapping. - We then call the underlying_method method after counting whether the - computed key value is already present in the memoization dictionary - (a hit) or not (a miss). """ - def __init__(self, method_name, keymaker): + def __init__(self, cls_name, method_name, keymaker): """ """ - Counter.__init__(self, method_name) + Counter.__init__(self, cls_name, method_name) self.keymaker = keymaker - def __call__(self, *args, **kw): + def count(self, *args, **kw): + """ Counts whether the computed key value is already present + in the memoization dictionary (a hit) or not (a miss). + """ obj = args[0] try: memo_dict = obj._memo[self.method_name] @@ -204,39 +179,65 @@ class CountDict(Counter): self.hit = self.hit + 1 else: self.miss = self.miss + 1 - return self.underlying_method(*args, **kw) - -class Memoizer(object): - """Object which performs caching of method calls for its 'primary' - instance.""" - - def __init__(self): - pass def Dump(title=None): + """ Dump the hit/miss count for all the counters + collected so far. + """ if title: print title - CounterList.sort() - for counter in CounterList: - counter.display() - -class Memoized_Metaclass(type): - def __init__(cls, name, bases, cls_dict): - super(Memoized_Metaclass, cls).__init__(name, bases, cls_dict) - - for counter in cls_dict.get('memoizer_counters', []): - method_name = counter.method_name - - counter.name = cls.__name__ + '.' + method_name - counter.underlying_method = cls_dict[method_name] - - replacement_method = types.MethodType(counter, None, cls) - setattr(cls, method_name, replacement_method) + for counter in sorted(CounterList): + CounterList[counter].display() def EnableMemoization(): global use_memoizer use_memoizer = 1 +def CountMethodCall(fn): + """ Decorator for counting memoizer hits/misses while retrieving + a simple value in a class method. It wraps the given method + fn and uses a CountValue object to keep track of the + caching statistics. + Wrapping gets enabled by calling EnableMemoization(). + """ + if use_memoizer: + def wrapper(self, *args, **kwargs): + global CounterList + key = self.__class__.__name__+'.'+fn.__name__ + if key not in CounterList: + CounterList[key] = CountValue(self.__class__.__name__, fn.__name__) + CounterList[key].count(self, *args, **kwargs) + return fn(self, *args, **kwargs) + wrapper.__name__= fn.__name__ + return wrapper + else: + return fn + +def CountDictCall(keyfunc): + """ Decorator for counting memoizer hits/misses while accessing + dictionary values with a key-generating function. Like + CountMethodCall above, it wraps the given method + fn and uses a CountDict object to keep track of the + caching statistics. The dict-key function keyfunc has to + get passed in the decorator call and gets stored in the + CountDict instance. + Wrapping gets enabled by calling EnableMemoization(). + """ + def decorator(fn): + if use_memoizer: + def wrapper(self, *args, **kwargs): + global CounterList + key = self.__class__.__name__+'.'+fn.__name__ + if key not in CounterList: + CounterList[key] = CountDict(self.__class__.__name__, fn.__name__, keyfunc) + CounterList[key].count(self, *args, **kwargs) + return fn(self, *args, **kwargs) + wrapper.__name__= fn.__name__ + return wrapper + else: + return fn + return decorator + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/src/engine/SCons/MemoizeTests.py b/src/engine/SCons/MemoizeTests.py index 3606d57..77ff6bc 100644 --- a/src/engine/SCons/MemoizeTests.py +++ b/src/engine/SCons/MemoizeTests.py @@ -30,22 +30,18 @@ import TestUnit import SCons.Memoize - +# Enable memoization counting +SCons.Memoize.EnableMemoization() class FakeObject(object): - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] - def __init__(self): self._memo = {} def _dict_key(self, argument): return argument - memoizer_counters.append(SCons.Memoize.CountDict('dict', _dict_key)) - + @SCons.Memoize.CountDictCall(_dict_key) def dict(self, argument): memo_key = argument @@ -66,8 +62,7 @@ class FakeObject(object): return result - memoizer_counters.append(SCons.Memoize.CountValue('value')) - + @SCons.Memoize.CountMethodCall def value(self): try: @@ -82,10 +77,7 @@ class FakeObject(object): return result def get_memoizer_counter(self, name): - for mc in self.memoizer_counters: - if mc.method_name == name: - return mc - return None + return SCons.Memoize.CounterList.get(self.__class__.__name__+'.'+name, None) class Returner(object): def __init__(self, result): diff --git a/src/engine/SCons/Node/Alias.py b/src/engine/SCons/Node/Alias.py index f817356..a035816 100644 --- a/src/engine/SCons/Node/Alias.py +++ b/src/engine/SCons/Node/Alias.py @@ -56,13 +56,47 @@ class AliasNameSpace(collections.UserDict): return None class AliasNodeInfo(SCons.Node.NodeInfoBase): - current_version_id = 1 + __slots__ = ('csig',) + current_version_id = 2 field_list = ['csig'] def str_to_node(self, s): return default_ans.Alias(s) + def __getstate__(self): + """ + Return all fields that shall be pickled. Walk the slots in the class + hierarchy and add those to the state dictionary. If a '__dict__' slot is + available, copy all entries to the dictionary. Also include the version + id, which is fixed for all instances of a class. + """ + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + + return state + + def __setstate__(self, state): + """ + Restore the attributes from a pickled state. + """ + # TODO check or discard version + del state['_version_id'] + for key, value in state.items(): + if key not in ('__weakref__',): + setattr(self, key, value) + + class AliasBuildInfo(SCons.Node.BuildInfoBase): - current_version_id = 1 + __slots__ = () + current_version_id = 2 class Alias(SCons.Node.Node): @@ -72,7 +106,9 @@ class Alias(SCons.Node.Node): def __init__(self, name): SCons.Node.Node.__init__(self) self.name = name - + self.changed_since_last_build = 1 + self.store_info = 0 + def str_for_display(self): return '"' + self.__str__() + '"' @@ -105,13 +141,6 @@ class Alias(SCons.Node.Node): # # - def changed_since_last_build(self, target, prev_ni): - cur_csig = self.get_csig() - try: - return cur_csig != prev_ni.csig - except AttributeError: - return 1 - def build(self): """A "builder" for aliases.""" pass diff --git a/src/engine/SCons/Node/AliasTests.py b/src/engine/SCons/Node/AliasTests.py index 2d11bdf..8e31875 100644 --- a/src/engine/SCons/Node/AliasTests.py +++ b/src/engine/SCons/Node/AliasTests.py @@ -103,14 +103,14 @@ class AliasNodeInfoTestCase(unittest.TestCase): """Test AliasNodeInfo initialization""" ans = SCons.Node.Alias.AliasNameSpace() aaa = ans.Alias('aaa') - ni = SCons.Node.Alias.AliasNodeInfo(aaa) + ni = SCons.Node.Alias.AliasNodeInfo() class AliasBuildInfoTestCase(unittest.TestCase): def test___init__(self): """Test AliasBuildInfo initialization""" ans = SCons.Node.Alias.AliasNameSpace() aaa = ans.Alias('aaa') - bi = SCons.Node.Alias.AliasBuildInfo(aaa) + bi = SCons.Node.Alias.AliasBuildInfo() if __name__ == "__main__": suite = unittest.TestSuite() diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index eec4e42..4e78852 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -56,10 +56,23 @@ import SCons.Warnings from SCons.Debug import Trace -do_store_info = True print_duplicate = 0 +def sconsign_none(node): + raise NotImplementedError + +def sconsign_dir(node): + """Return the .sconsign file info for this directory, + creating it first if necessary.""" + if not node._sconsign: + import SCons.SConsign + node._sconsign = SCons.SConsign.ForDirectory(node) + return node._sconsign + +_sconsign_map = {0 : sconsign_none, + 1 : sconsign_dir} + class EntryProxyAttributeError(AttributeError): """ An AttributeError subclass for recording and displaying the name @@ -268,8 +281,8 @@ def LinkFunc(target, source, env): # 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].abspath - dest = target[0].abspath + src = source[0].get_abspath() + dest = target[0].get_abspath() dir, file = os.path.split(dest) if dir and not target[0].fs.isdir(dir): os.makedirs(dir) @@ -302,7 +315,7 @@ LocalCopy = SCons.Action.Action(LinkFunc, LocalString) def UnlinkFunc(target, source, env): t = target[0] - t.fs.unlink(t.abspath) + t.fs.unlink(t.get_abspath()) return 0 Unlink = SCons.Action.Action(UnlinkFunc, None) @@ -310,7 +323,7 @@ Unlink = SCons.Action.Action(UnlinkFunc, None) def MkdirFunc(target, source, env): t = target[0] if not t.exists(): - t.fs.mkdir(t.abspath) + t.fs.mkdir(t.get_abspath()) return 0 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None) @@ -403,7 +416,7 @@ def do_diskcheck_match(node, predicate, errorfmt): except (AttributeError, KeyError): pass if result: - raise TypeError(errorfmt % node.abspath) + raise TypeError(errorfmt % node.get_abspath()) def ignore_diskcheck_match(node, predicate, errorfmt): pass @@ -573,7 +586,20 @@ class Base(SCons.Node.Node): object identity comparisons. """ - memoizer_counters = [] + __slots__ = ['name', + 'fs', + '_abspath', + '_labspath', + '_path', + '_tpath', + '_path_elements', + 'dir', + 'cwd', + 'duplicate', + '_local', + 'sbuilder', + '_proxy', + '_func_sconsign'] def __init__(self, name, directory, fs): """Initialize a generic Node.FS.Base object. @@ -591,27 +617,26 @@ class Base(SCons.Node.Node): #: Filename with extension as it was specified when the object was #: created; to obtain filesystem path, use Python str() function self.name = SCons.Util.silent_intern(name) - #: Cached filename extension - self.suffix = SCons.Util.silent_intern(SCons.Util.splitext(name)[1]) self.fs = fs #: Reference to parent Node.FS object assert directory, "A directory must be provided" - 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 = SCons.Util.silent_intern(name) - else: - self.path = SCons.Util.silent_intern(directory.entry_path(name)) - if directory.tpath == '.': - self.tpath = SCons.Util.silent_intern(name) - else: - self.tpath = SCons.Util.silent_intern(directory.entry_tpath(name)) - self.path_elements = directory.path_elements + [self] + self._abspath = None + self._labspath = None + self._path = None + self._tpath = None + self._path_elements = None self.dir = directory self.cwd = None # will hold the SConscript directory for target nodes self.duplicate = directory.duplicate + self.changed_since_last_build = 2 + self._func_sconsign = 0 + self._func_exists = 2 + self._func_rexists = 2 + self._func_get_contents = 0 + self._func_target_from_source = 1 + self.store_info = 1 def str_for_display(self): return '"' + self.__str__() + '"' @@ -624,17 +649,38 @@ class Base(SCons.Node.Node): 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__)) + (self.__class__.__name__, self.get_internal_path(), klass.__name__)) def get_dir(self): return self.dir def get_suffix(self): - return self.suffix + return SCons.Util.splitext(self.name)[1] def rfile(self): return self + def __getattr__(self, attr): + """ Together with the node_bwcomp dict defined below, + this method provides a simple backward compatibility + layer for the Node attributes 'abspath', 'labspath', + 'path', 'tpath', 'suffix' and 'path_elements'. These Node + attributes used to be directly available in v2.3 and earlier, but + have been replaced by getter methods that initialize the + single variables lazily when required, in order to save memory. + The redirection to the getters lets older Tools and + SConstruct continue to work without any additional changes, + fully transparent to the user. + Note, that __getattr__ is only called as fallback when the + requested attribute can't be found, so there should be no + speed performance penalty involved for standard builds. + """ + if attr in node_bwcomp: + return node_bwcomp[attr](self) + + raise AttributeError("%r object has no attribute %r" % + (self.__class__, attr)) + def __str__(self): """A Node.FS.Base object's string representation is its path name.""" @@ -643,8 +689,7 @@ class Base(SCons.Node.Node): return self._save_str() return self._get_str() - memoizer_counters.append(SCons.Memoize.CountValue('_save_str')) - + @SCons.Memoize.CountMethodCall def _save_str(self): try: return self._memo['_save_str'] @@ -681,21 +726,20 @@ class Base(SCons.Node.Node): rstr = __str__ - memoizer_counters.append(SCons.Memoize.CountValue('stat')) - + @SCons.Memoize.CountMethodCall def stat(self): try: return self._memo['stat'] except KeyError: pass - try: result = self.fs.stat(self.abspath) + try: result = self.fs.stat(self.get_abspath()) except os.error: result = None self._memo['stat'] = result return result def exists(self): - return self.stat() is not None + return SCons.Node._exists_map[self._func_exists](self) def rexists(self): - return self.rfile().exists() + return SCons.Node._rexists_map[self._func_rexists](self) def getmtime(self): st = self.stat() @@ -717,7 +761,7 @@ class Base(SCons.Node.Node): if hasattr(os, 'symlink'): def islink(self): - try: st = self.fs.lstat(self.abspath) + try: st = self.fs.lstat(self.get_abspath()) except os.error: return 0 return stat.S_ISLNK(st[stat.ST_MODE]) else: @@ -752,7 +796,7 @@ class Base(SCons.Node.Node): dir = self.fs.getcwd() if self == dir: return '.' - path_elems = self.path_elements + path_elems = self.get_path_elements() pathname = '' try: i = path_elems.index(dir) except ValueError: @@ -785,7 +829,26 @@ class Base(SCons.Node.Node): def get_abspath(self): """Get the absolute path of the file.""" - return self.abspath + return self.dir.entry_abspath(self.name) + + def get_labspath(self): + """Get the absolute path of the file.""" + return self.dir.entry_labspath(self.name) + + def get_internal_path(self): + if self.dir._path == '.': + return self.name + else: + return self.dir.entry_path(self.name) + + def get_tpath(self): + if self.dir._tpath == '.': + return self.name + else: + return self.dir.entry_tpath(self.name) + + def get_path_elements(self): + return self.dir._path_elements + [self] def for_signature(self): # Return just our name. Even an absolute path would not work, @@ -811,13 +874,12 @@ class Base(SCons.Node.Node): files that need different behavior. See Tool/swig.py for an example. """ - return self.dir.Entry(prefix + splitext(self.name)[0] + suffix) + return SCons.Node._target_from_source_map[self._func_target_from_source](self, prefix, suffix, splitext) def _Rfindalldirs_key(self, pathlist): return pathlist - memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key)) - + @SCons.Memoize.CountDictCall(_Rfindalldirs_key) def Rfindalldirs(self, pathlist): """ Return all of the directories for a given path list, including @@ -856,8 +918,7 @@ class Base(SCons.Node.Node): cwd = self.cwd or self.fs._cwd return cwd.Rfindalldirs(pathlist) - memoizer_counters.append(SCons.Memoize.CountValue('rentry')) - + @SCons.Memoize.CountMethodCall def rentry(self): try: return self._memo['rentry'] @@ -878,6 +939,17 @@ class Base(SCons.Node.Node): def _glob1(self, pattern, ondisk=True, source=False, strings=False): return [] + +# Dict that provides a simple backward compatibility +# layer for the Node attributes 'abspath', 'labspath', +# 'path', 'tpath' and 'path_elements'. +# @see Base.__getattr__ above +node_bwcomp = {'abspath' : Base.get_abspath, + 'labspath' : Base.get_labspath, + 'path' : Base.get_internal_path, + 'tpath' : Base.get_tpath, + 'path_elements' : Base.get_path_elements, + 'suffix' : Base.get_suffix} class Entry(Base): """This is the class for generic Node.FS entries--that is, things @@ -887,6 +959,28 @@ class Entry(Base): time comes, and then call the same-named method in the transformed class.""" + __slots__ = ['scanner_paths', + 'cachedir_csig', + 'cachesig', + 'repositories', + 'srcdir', + 'entries', + 'searched', + '_sconsign', + 'variant_dirs', + 'root', + 'dirname', + 'on_disk_entries', + 'sccs_dir', + 'rcs_dir', + 'released_target_info', + 'contentsig'] + + def __init__(self, name, directory, fs): + Base.__init__(self, name, directory, fs) + self._func_exists = 3 + self._func_get_contents = 1 + def diskcheck_match(self): pass @@ -917,7 +1011,7 @@ class Entry(Base): self.__class__ = Dir self._morph() elif must_exist: - msg = "No such file or directory: '%s'" % self.abspath + msg = "No such file or directory: '%s'" % self.get_abspath() raise SCons.Errors.UserError(msg) else: self.__class__ = File @@ -939,17 +1033,7 @@ class Entry(Base): def get_contents(self): """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: - # 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_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_contents() + return SCons.Node._get_contents_map[self._func_get_contents](self) def get_text_contents(self): """Fetch the decoded text contents of a Unicode encoded Entry. @@ -989,10 +1073,7 @@ class Entry(Base): # to make various tests pass. def exists(self): - """Return if the Entry exists. Check the file system to see - what we should turn into first. Assume a file if there's no - directory.""" - return self.disambiguate().exists() + return SCons.Node._exists_map[self._func_exists](self) def rel_path(self, other): d = self.disambiguate() @@ -1003,9 +1084,6 @@ class Entry(Base): def new_ninfo(self): return self.disambiguate().new_ninfo() - def changed_since_last_build(self, target, prev_ni): - return self.disambiguate().changed_since_last_build(target, prev_ni) - def _glob1(self, pattern, ondisk=True, source=False, strings=False): return self.disambiguate()._glob1(pattern, ondisk, source, strings) @@ -1019,9 +1097,6 @@ _classEntry = Entry class LocalFS(object): - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - # 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 @@ -1101,8 +1176,6 @@ class LocalFS(object): class FS(LocalFS): - memoizer_counters = [] - def __init__(self, path = None): """Initialize the Node.FS subsystem. @@ -1128,8 +1201,8 @@ class FS(LocalFS): self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0]) self.Top = self.Dir(self.pathTop) - self.Top.path = '.' - self.Top.tpath = '.' + self.Top._path = '.' + self.Top._tpath = '.' self._cwd = self.Top DirNodeInfo.fs = self @@ -1160,7 +1233,7 @@ class FS(LocalFS): if dir is not None: self._cwd = dir if change_os_dir: - os.chdir(dir.abspath) + os.chdir(dir.get_abspath()) except OSError: self._cwd = curr raise @@ -1249,9 +1322,9 @@ class FS(LocalFS): # The path is relative to the top-level SCons directory. if p in ('', '.'): - p = directory.labspath + p = directory.get_labspath() else: - p = directory.labspath + '/' + p + p = directory.get_labspath() + '/' + p else: if do_splitdrive: drive, p = _my_splitdrive(p) @@ -1285,9 +1358,9 @@ class FS(LocalFS): directory = self._cwd if p in ('', '.'): - p = directory.labspath + p = directory.get_labspath() else: - p = directory.labspath + '/' + p + p = directory.get_labspath() + '/' + p if drive: root = self.get_root(drive) @@ -1393,7 +1466,7 @@ class FS(LocalFS): if start_dir.is_under(bd): # If already in the build-dir location, don't reflect return [orig], fmt % str(orig) - p = 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() @@ -1412,8 +1485,9 @@ class FS(LocalFS): return cwd.glob(pathname, ondisk, source, strings, exclude) class DirNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = () # This should get reset by the FS initialization. - current_version_id = 1 + current_version_id = 2 fs = None @@ -1425,11 +1499,12 @@ class DirNodeInfo(SCons.Node.NodeInfoBase): if drive: root = self.fs.get_root(drive) if not os.path.isabs(s): - s = top.labspath + '/' + s + s = top.get_labspath() + '/' + s return root._lookup_abs(s, Entry) class DirBuildInfo(SCons.Node.BuildInfoBase): - current_version_id = 1 + __slots__ = () + current_version_id = 2 glob_magic_check = re.compile('[*?[]') @@ -1440,7 +1515,22 @@ class Dir(Base): """A class for directories in a file system. """ - memoizer_counters = [] + __slots__ = ['scanner_paths', + 'cachedir_csig', + 'cachesig', + 'repositories', + 'srcdir', + 'entries', + 'searched', + '_sconsign', + 'variant_dirs', + 'root', + 'dirname', + 'on_disk_entries', + 'sccs_dir', + 'rcs_dir', + 'released_target_info', + 'contentsig'] NodeInfo = DirNodeInfo BuildInfo = DirBuildInfo @@ -1470,6 +1560,22 @@ class Dir(Base): self._sconsign = None self.variant_dirs = [] self.root = self.dir.root + self.changed_since_last_build = 3 + self._func_sconsign = 1 + self._func_exists = 2 + self._func_get_contents = 2 + + self._abspath = SCons.Util.silent_intern(self.dir.entry_abspath(self.name)) + self._labspath = SCons.Util.silent_intern(self.dir.entry_labspath(self.name)) + if self.dir._path == '.': + self._path = SCons.Util.silent_intern(self.name) + else: + self._path = SCons.Util.silent_intern(self.dir.entry_path(self.name)) + if self.dir._tpath == '.': + self._tpath = SCons.Util.silent_intern(self.name) + else: + self._tpath = SCons.Util.silent_intern(self.dir.entry_tpath(self.name)) + self._path_elements = self.dir._path_elements + [self] # For directories, we make a difference between the directory # 'name' and the directory 'dirname'. The 'name' attribute is @@ -1562,8 +1668,7 @@ class Dir(Base): return self.srcdir.get_all_rdirs() + self.repositories return self.repositories - memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs')) - + @SCons.Memoize.CountMethodCall def get_all_rdirs(self): try: return list(self._memo['get_all_rdirs']) @@ -1589,7 +1694,7 @@ class Dir(Base): def addRepository(self, dir): if dir != self and not dir in self.repositories: self.repositories.append(dir) - dir.tpath = '.' + dir._tpath = '.' self.__clearRepositoryCache() def up(self): @@ -1598,8 +1703,7 @@ class Dir(Base): def _rel_path_key(self, other): return str(other) - memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key)) - + @SCons.Memoize.CountDictCall(_rel_path_key) def rel_path(self, other): """Return a path to "other" relative to this directory. """ @@ -1628,7 +1732,7 @@ class Dir(Base): if self is other: result = '.' - elif not other in self.path_elements: + elif not other in self._path_elements: try: other_dir = other.get_dir() except AttributeError: @@ -1643,10 +1747,10 @@ class Dir(Base): else: result = dir_rel_path + OS_SEP + other.name else: - i = self.path_elements.index(other) + 1 + i = self._path_elements.index(other) + 1 - path_elems = ['..'] * (len(self.path_elements) - i) \ - + [n.name for n in other.path_elements[i:]] + path_elems = ['..'] * (len(self._path_elements) - i) \ + + [n.name for n in other._path_elements[i:]] result = OS_SEP.join(path_elems) @@ -1713,7 +1817,7 @@ class Dir(Base): 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) + raise SCons.Errors.StopError(parent._path) parent = p listDirs.reverse() for dirnode in listDirs: @@ -1753,10 +1857,7 @@ class Dir(Base): def get_contents(self): """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) + return SCons.Node._get_contents_map[self._func_get_contents](self) def get_csig(self): """Compute the content signature for Directory nodes. In @@ -1770,8 +1871,6 @@ class Dir(Base): def do_duplicate(self, src): pass - changed_since_last_build = SCons.Node.Node.state_has_changed - def is_up_to_date(self): """If any child is not up-to-date, then this directory isn't, either.""" @@ -1795,12 +1894,8 @@ class Dir(Base): return self def sconsign(self): - """Return the .sconsign file info for this directory, - creating it first if necessary.""" - if not self._sconsign: - import SCons.SConsign - self._sconsign = SCons.SConsign.ForDirectory(self) - return self._sconsign + """Return the .sconsign file info for this directory. """ + return _sconsign_map[self._func_sconsign](self) def srcnode(self): """Dir has a special need for srcnode()...if we @@ -1817,17 +1912,34 @@ class Dir(Base): stamp = kid.get_timestamp() return stamp + def get_abspath(self): + """Get the absolute path of the file.""" + return self._abspath + + def get_labspath(self): + """Get the absolute path of the file.""" + return self._labspath + + def get_internal_path(self): + return self._path + + def get_tpath(self): + return self._tpath + + def get_path_elements(self): + return self._path_elements + def entry_abspath(self, name): - return self.abspath + OS_SEP + name + return self._abspath + OS_SEP + name def entry_labspath(self, name): - return self.labspath + '/' + name + return self._labspath + '/' + name def entry_path(self, name): - return self.path + OS_SEP + name + return self._path + OS_SEP + name def entry_tpath(self, name): - return self.tpath + OS_SEP + name + return self._tpath + OS_SEP + name def entry_exists_on_disk(self, name): """ Searches through the file/dir entries of the current @@ -1841,7 +1953,7 @@ class Dir(Base): except AttributeError: d = {} try: - entries = os.listdir(self.abspath) + entries = os.listdir(self._abspath) except OSError: pass else: @@ -1854,7 +1966,7 @@ class Dir(Base): 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) + result = os.path.exists(self._abspath + OS_SEP + name) d[name] = result return result else: @@ -1887,8 +1999,7 @@ class Dir(Base): break return rentry_exists - memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list')) - + @SCons.Memoize.CountMethodCall def srcdir_list(self): try: return self._memo['srcdir_list'] @@ -1929,8 +2040,7 @@ class Dir(Base): def _srcdir_find_file_key(self, filename): return filename - memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key)) - + @SCons.Memoize.CountDictCall(_srcdir_find_file_key) def srcdir_find_file(self, filename): try: memo_dict = self._memo['srcdir_find_file'] @@ -2072,7 +2182,12 @@ class Dir(Base): r = [os.path.join(str(dir), x) for x in r] result.extend(r) if exclude: - result = filter(lambda x: not any(fnmatch.fnmatch(str(x), e) for e in SCons.Util.flatten(exclude)), result) + excludes = [] + excludeList = SCons.Util.flatten(exclude) + for x in excludeList: + r = self.glob(x, ondisk, source, strings) + excludes.extend(r) + result = filter(lambda x: not any(fnmatch.fnmatch(str(x), str(e)) for e in SCons.Util.flatten(excludes)), result) return sorted(result, key=lambda a: str(a)) def _glob1(self, pattern, ondisk=True, source=False, strings=False): @@ -2105,7 +2220,7 @@ class Dir(Base): for name in node_names: selfEntry(name) if ondisk: try: - disk_names = os.listdir(dir.abspath) + disk_names = os.listdir(dir._abspath) except os.error: continue names.extend(disk_names) @@ -2151,18 +2266,12 @@ class RootDir(Dir): add a separator when creating the path names of entries within this directory. """ + + __slots__ = ['_lookupDict'] + def __init__(self, drive, fs): if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.FS.RootDir') - # We're going to be our own parent directory (".." entry and .dir - # attribute) so we have to set up some values so Base.__init__() - # won't gag won't it calls some of our methods. - self.abspath = '' - self.labspath = '' - self.path = '' - self.tpath = '' - self.path_elements = [] - self.duplicate = 0 - self.root = self + SCons.Node.Node.__init__(self) # Handle all the types of drives: if drive == '': @@ -2178,33 +2287,85 @@ class RootDir(Dir): name = drive dirname = drive + OS_SEP - Base.__init__(self, name, self, fs) + #: Filename with extension as it was specified when the object was + #: created; to obtain filesystem path, use Python str() function + self.name = SCons.Util.silent_intern(name) + self.fs = fs #: Reference to parent Node.FS object + + self._path_elements = [self] + self.dir = self + self._func_rexists = 2 + self._func_target_from_source = 1 + self.store_info = 1 # Now set our paths to what we really want them to be. The # name should already contain any necessary separators, such # as the initial drive letter (the name) plus the directory # separator, except for the "lookup abspath," which does not # have the drive letter. - self.abspath = dirname - self.labspath = '' - self.path = dirname - self.tpath = dirname - self._morph() - - # Must be reset after Dir._morph() is invoked... + self._abspath = dirname + self._labspath = '' + self._path = dirname + self._tpath = dirname self.dirname = dirname + self._morph() + + self.duplicate = 0 self._lookupDict = {} self._lookupDict[''] = self self._lookupDict['/'] = self - + self.root = self # The // entry is necessary because os.path.normpath() # preserves double slashes at the beginning of a path on Posix # platforms. if not has_unc: self._lookupDict['//'] = self + def _morph(self): + """Turn a file system Node (either a freshly initialized directory + object or a separate Entry object) into a proper directory object. + + Set up this directory's entries and hook it into the file + system tree. Specify that directories (this Node) don't use + signatures for calculating whether they're current. + """ + + self.repositories = [] + self.srcdir = None + + self.entries = {} + self.entries['.'] = self + self.entries['..'] = self.dir + self.cwd = self + self.searched = 0 + self._sconsign = None + self.variant_dirs = [] + self.changed_since_last_build = 3 + self._func_sconsign = 1 + self._func_exists = 2 + self._func_get_contents = 2 + + # Don't just reset the executor, replace its action list, + # because it might have some pre-or post-actions that need to + # be preserved. + # + # But don't reset the executor if there is a non-null executor + # attached already. The existing executor might have other + # targets, in which case replacing the action list with a + # Mkdir action is a big mistake. + if not hasattr(self, 'executor'): + self.builder = get_MkdirBuilder() + self.get_executor().set_action_list(self.builder.action) + else: + # Prepend MkdirBuilder action to existing action list + l = self.get_executor().action_list + a = get_MkdirBuilder().action + l.insert(0, a) + self.get_executor().set_action_list(l) + + def must_be_same(self, klass): if klass is Dir: return @@ -2253,19 +2414,19 @@ class RootDir(Dir): return result def __str__(self): - return self.abspath + return self._abspath def entry_abspath(self, name): - return self.abspath + name + return self._abspath + name def entry_labspath(self, name): return '/' + name def entry_path(self, name): - return self.path + name + return self._path + name def entry_tpath(self, name): - return self.tpath + name + return self._tpath + name def is_under(self, dir): if self is dir: @@ -2283,7 +2444,8 @@ class RootDir(Dir): return _null class FileNodeInfo(SCons.Node.NodeInfoBase): - current_version_id = 1 + __slots__ = ('csig', 'timestamp', 'size') + current_version_id = 2 field_list = ['csig', 'timestamp', 'size'] @@ -2298,11 +2460,43 @@ class FileNodeInfo(SCons.Node.NodeInfoBase): if drive: root = self.fs.get_root(drive) if not os.path.isabs(s): - s = top.labspath + '/' + s + s = top.get_labspath() + '/' + s return root._lookup_abs(s, Entry) + def __getstate__(self): + """ + Return all fields that shall be pickled. Walk the slots in the class + hierarchy and add those to the state dictionary. If a '__dict__' slot is + available, copy all entries to the dictionary. Also include the version + id, which is fixed for all instances of a class. + """ + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + + return state + + def __setstate__(self, state): + """ + Restore the attributes from a pickled state. + """ + # TODO check or discard version + del state['_version_id'] + for key, value in state.items(): + if key not in ('__weakref__',): + setattr(self, key, value) + class FileBuildInfo(SCons.Node.BuildInfoBase): - current_version_id = 1 + __slots__ = () + current_version_id = 2 def convert_to_sconsign(self): """ @@ -2317,7 +2511,7 @@ class FileBuildInfo(SCons.Node.BuildInfoBase): else: def node_to_str(n): try: - s = n.path + s = n.get_internal_path() except AttributeError: s = str(n) else: @@ -2358,6 +2552,8 @@ class FileBuildInfo(SCons.Node.BuildInfoBase): nodeinfos = getattr(self, sattr) except AttributeError: continue + if strings is None or nodeinfos is None: + continue nodes = [] for s, ni in zip(strings, nodeinfos): if not isinstance(s, SCons.Node.Node): @@ -2371,6 +2567,8 @@ class FileBuildInfo(SCons.Node.BuildInfoBase): for bkid, bkidsig in zip(bkids, bkidsigs): result.append(str(bkid) + ': ' + ' '.join(bkidsig.format(names=names))) + if not hasattr(self,'bact'): + self.bact = "none" result.append('%s [%s]' % (self.bactsig, self.bact)) return '\n'.join(result) @@ -2378,7 +2576,22 @@ class File(Base): """A class for files in a file system. """ - memoizer_counters = [] + __slots__ = ['scanner_paths', + 'cachedir_csig', + 'cachesig', + 'repositories', + 'srcdir', + 'entries', + 'searched', + '_sconsign', + 'variant_dirs', + 'root', + 'dirname', + 'on_disk_entries', + 'sccs_dir', + 'rcs_dir', + 'released_target_info', + 'contentsig'] NodeInfo = FileNodeInfo BuildInfo = FileBuildInfo @@ -2429,6 +2642,14 @@ class File(Base): if not hasattr(self, 'released_target_info'): self.released_target_info = False + self.store_info = 1 + self._func_exists = 4 + self._func_get_contents = 3 + + # Initialize this Node's decider function to decide_source() because + # every file is a source file until it has a Builder attached... + self.changed_since_last_build = 4 + # If there was already a Builder set on this entry, then # we need to make sure we call the target-decider function, # not the source-decider. Reaching in and doing this by hand @@ -2440,22 +2661,13 @@ class File(Base): # not clear right now how to fix that, stick with what works # until it becomes clear... if self.has_builder(): - self.changed_since_last_build = self.decide_target + self.changed_since_last_build = 5 def scanner_key(self): return self.get_suffix() def get_contents(self): - if not self.rexists(): - return '' - fname = self.rfile().abspath - try: - contents = open(fname, "rb").read() - except EnvironmentError, e: - if not e.filename: - e.filename = fname - raise - return contents + return SCons.Node._get_contents_map[self._func_get_contents](self) # This attempts to figure out what the encoding of the text is # based upon the BOM bytes, and then decodes the contents so that @@ -2482,7 +2694,7 @@ class File(Base): """ if not self.rexists(): return SCons.Util.MD5signature('') - fname = self.rfile().abspath + fname = self.rfile().get_abspath() try: cs = SCons.Util.MD5filesignature(fname, chunksize=SCons.Node.FS.File.md5_chunksize*1024) @@ -2492,9 +2704,7 @@ class File(Base): raise return cs - - memoizer_counters.append(SCons.Memoize.CountValue('get_size')) - + @SCons.Memoize.CountMethodCall def get_size(self): try: return self._memo['get_size'] @@ -2510,8 +2720,7 @@ class File(Base): return size - memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp')) - + @SCons.Memoize.CountMethodCall def get_timestamp(self): try: return self._memo['get_timestamp'] @@ -2527,14 +2736,6 @@ class File(Base): return timestamp - def store_info(self): - # Merge our build information into the already-stored entry. - # 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. - if do_store_info: - self.dir.sconsign().store_info(self.name, self) - convert_copy_attrs = [ 'bsources', 'bimplicit', @@ -2647,8 +2848,7 @@ class File(Base): delattr(old_entry, attr) return new_entry - memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info')) - + @SCons.Memoize.CountMethodCall def get_stored_info(self): try: return self._memo['get_stored_info'] @@ -2688,8 +2888,7 @@ class File(Base): def _get_found_includes_key(self, env, scanner, path): return (id(env), id(scanner), path) - memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key)) - + @SCons.Memoize.CountDictCall(_get_found_includes_key) def get_found_includes(self, env, scanner, path): """Return the included implicit dependencies in this file. Cache results so we only scan the file once per path @@ -2773,9 +2972,9 @@ class File(Base): # 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.get_binfo().merge(old.binfo) - self.store_info() + SCons.Node.store_info_map[self.store_info](self) def release_target_info(self): """Called just after this node has been marked @@ -2875,7 +3074,7 @@ class File(Base): def _rmv_existing(self): self.clear_memoized_values() - if print_duplicate: + if SCons.Node.print_duplicate: print "dup: removing existing target %s"%self e = Unlink(self, [], None) if isinstance(e, SCons.Errors.BuildError): @@ -2911,18 +3110,18 @@ class File(Base): def remove(self): """Remove this file.""" if self.exists() or self.islink(): - self.fs.unlink(self.path) + self.fs.unlink(self.get_internal_path()) return 1 return None def do_duplicate(self, src): self._createDir() - if print_duplicate: + if SCons.Node.print_duplicate: print "dup: relinking variant '%s' from '%s'"%(self, src) Unlink(self, None, None) e = Link(self, src, None) if isinstance(e, SCons.Errors.BuildError): - desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr) + desc = "Cannot duplicate `%s' in `%s': %s." % (src.get_internal_path(), self.dir._path, e.errstr) raise SCons.Errors.StopError(desc) self.linked = 1 # The Link() action may or may not have actually @@ -2931,36 +3130,14 @@ class File(Base): # _rexists attributes so they can be reevaluated. self.clear() - memoizer_counters.append(SCons.Memoize.CountValue('exists')) - + @SCons.Memoize.CountMethodCall def exists(self): try: return self._memo['exists'] except KeyError: pass - # 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 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(): - self.do_duplicate(src) - # Can't return 1 here because the duplication might - # 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 variant directory. - if print_duplicate: - print "dup: no src for %s, unlinking old variant copy"%self - if Base.exists(self) or self.islink(): - self.fs.unlink(self.path) - # Return None explicitly because the Base.exists() call - # above will have cached its value if the file existed. - self._memo['exists'] = None - return None - result = Base.exists(self) + + result = SCons.Node._exists_map[self._func_exists](self) self._memo['exists'] = result return result @@ -3037,7 +3214,7 @@ class File(Base): def builder_set(self, builder): SCons.Node.Node.builder_set(self, builder) - self.changed_since_last_build = self.decide_target + self.changed_since_last_build = 5 def built(self): """Called just after this File node is successfully built. @@ -3054,10 +3231,10 @@ class File(Base): if (not SCons.Node.interactive and not hasattr(self.attributes, 'keep_targetinfo')): # Ensure that the build infos get computed and cached... - self.store_info() + SCons.Node.store_info_map[self.store_info](self) # ... then release some more variables. self._specific_sources = False - self.labspath = None + self._labspath = None self._save_str() self.cwd = None @@ -3116,16 +3293,6 @@ class File(Base): except AttributeError: return 1 - def decide_source(self, target, prev_ni): - return target.get_build_env().decide_source(self, target, prev_ni) - - def decide_target(self, target, prev_ni): - return target.get_build_env().decide_target(self, target, prev_ni) - - # Initialize this Node's decider function to decide_source() because - # every file is a source file until it has a Builder attached... - changed_since_last_build = decide_source - def is_up_to_date(self): T = 0 if T: Trace('is_up_to_date(%s):' % self) @@ -3143,7 +3310,7 @@ class File(Base): e = LocalCopy(self, r, None) if isinstance(e, SCons.Errors.BuildError): raise - self.store_info() + SCons.Node.store_info_map[self.store_info](self) if T: Trace(' 1\n') return 1 self.changed() @@ -3154,8 +3321,7 @@ class File(Base): if T: Trace(' self.exists(): %s\n' % r) return not r - memoizer_counters.append(SCons.Memoize.CountValue('rfile')) - + @SCons.Memoize.CountMethodCall def rfile(self): try: return self._memo['rfile'] @@ -3257,7 +3423,7 @@ class File(Base): # Append this node's signature... sigs.append(self.get_contents_sig()) # ...and it's path - sigs.append(self.path) + sigs.append(self.get_internal_path()) # Merge this all into a single signature result = self.cachesig = SCons.Util.MD5collect(sigs) return result @@ -3273,10 +3439,6 @@ def get_default_fs(): class FileFinder(object): """ """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] def __init__(self): self._memo = {} @@ -3319,8 +3481,7 @@ class FileFinder(object): def _find_file_key(self, filename, paths, verbose=None): return (filename, paths) - memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key)) - + @SCons.Memoize.CountDictCall(_find_file_key) def find_file(self, filename, paths, verbose=None): """ find_file(str, [Dir()]) -> [nodes] diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 0326717..0c1e71f 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -130,34 +130,34 @@ class VariantDirTestCase(unittest.TestCase): fs.VariantDir('build', 'src') f2 = fs.File('build/test2') d1 = fs.Dir('build') - assert f1.srcnode().path == os.path.normpath('src/test1'), f1.srcnode().path - assert f2.srcnode().path == os.path.normpath('src/test2'), f2.srcnode().path - assert d1.srcnode().path == 'src', d1.srcnode().path + assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path() + assert f2.srcnode().get_internal_path() == os.path.normpath('src/test2'), f2.srcnode().get_internal_path() + assert d1.srcnode().get_internal_path() == 'src', d1.srcnode().get_internal_path() fs = SCons.Node.FS.FS() f1 = fs.File('build/test1') fs.VariantDir('build', '.') f2 = fs.File('build/test2') d1 = fs.Dir('build') - assert f1.srcnode().path == 'test1', f1.srcnode().path - assert f2.srcnode().path == 'test2', f2.srcnode().path - assert d1.srcnode().path == '.', d1.srcnode().path + assert f1.srcnode().get_internal_path() == 'test1', f1.srcnode().get_internal_path() + assert f2.srcnode().get_internal_path() == 'test2', f2.srcnode().get_internal_path() + assert d1.srcnode().get_internal_path() == '.', d1.srcnode().get_internal_path() fs = SCons.Node.FS.FS() fs.VariantDir('build/var1', 'src') fs.VariantDir('build/var2', 'src') f1 = fs.File('build/var1/test1') f2 = fs.File('build/var2/test1') - assert f1.srcnode().path == os.path.normpath('src/test1'), f1.srcnode().path - assert f2.srcnode().path == os.path.normpath('src/test1'), f2.srcnode().path + assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path() + assert f2.srcnode().get_internal_path() == os.path.normpath('src/test1'), f2.srcnode().get_internal_path() fs = SCons.Node.FS.FS() fs.VariantDir('../var1', 'src') fs.VariantDir('../var2', 'src') f1 = fs.File('../var1/test1') f2 = fs.File('../var2/test1') - assert f1.srcnode().path == os.path.normpath('src/test1'), f1.srcnode().path - assert f2.srcnode().path == os.path.normpath('src/test1'), f2.srcnode().path + assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path() + assert f2.srcnode().get_internal_path() == os.path.normpath('src/test1'), f2.srcnode().get_internal_path() # Set up some files test.subdir('work', ['work', 'src']) @@ -210,8 +210,8 @@ class VariantDirTestCase(unittest.TestCase): f2out_2.builder = 1 fs.Repository(test.workpath('rep1')) - assert f1.srcnode().path == os.path.normpath('src/test.in'),\ - f1.srcnode().path + assert f1.srcnode().get_internal_path() == os.path.normpath('src/test.in'),\ + f1.srcnode().get_internal_path() # str(node) returns source path for duplicate = 0 assert str(f1) == os.path.normpath('src/test.in'), str(f1) # Build path does not exist @@ -221,11 +221,11 @@ class VariantDirTestCase(unittest.TestCase): # And duplicate=0 should also work just like a Repository assert f1.rexists() # rfile() should point to the source path - assert f1.rfile().path == os.path.normpath('src/test.in'),\ - f1.rfile().path + assert f1.rfile().get_internal_path() == os.path.normpath('src/test.in'),\ + f1.rfile().get_internal_path() - assert f2.srcnode().path == os.path.normpath('src/test.in'),\ - f2.srcnode().path + assert f2.srcnode().get_internal_path() == os.path.normpath('src/test.in'),\ + f2.srcnode().get_internal_path() # str(node) returns build path for duplicate = 1 assert str(f2) == os.path.normpath('build/var2/test.in'), str(f2) # Build path exists @@ -239,8 +239,8 @@ class VariantDirTestCase(unittest.TestCase): f3 = fs.File('build/var1/test2.in') f4 = fs.File('build/var2/test2.in') - assert f3.srcnode().path == os.path.normpath('src/test2.in'),\ - f3.srcnode().path + assert f3.srcnode().get_internal_path() == os.path.normpath('src/test2.in'),\ + f3.srcnode().get_internal_path() # str(node) returns source path for duplicate = 0 assert str(f3) == os.path.normpath('src/test2.in'), str(f3) # Build path does not exist @@ -250,11 +250,11 @@ class VariantDirTestCase(unittest.TestCase): # But we do have a file in the Repository assert f3.rexists() # rfile() should point to the source path - assert f3.rfile().path == os.path.normpath(test.workpath('rep1/src/test2.in')),\ - f3.rfile().path + assert f3.rfile().get_internal_path() == os.path.normpath(test.workpath('rep1/src/test2.in')),\ + f3.rfile().get_internal_path() - assert f4.srcnode().path == os.path.normpath('src/test2.in'),\ - f4.srcnode().path + assert f4.srcnode().get_internal_path() == os.path.normpath('src/test2.in'),\ + f4.srcnode().get_internal_path() # str(node) returns build path for duplicate = 1 assert str(f4) == os.path.normpath('build/var2/test2.in'), str(f4) # Build path should exist @@ -264,8 +264,8 @@ class VariantDirTestCase(unittest.TestCase): # should exist in repository, since exists() is true assert f4.rexists() # rfile() should point to ourselves - assert f4.rfile().path == os.path.normpath('build/var2/test2.in'),\ - f4.rfile().path + assert f4.rfile().get_internal_path() == os.path.normpath('build/var2/test2.in'),\ + f4.rfile().get_internal_path() f5 = fs.File('build/var1/test.out') f6 = fs.File('build/var2/test.out') @@ -285,14 +285,14 @@ class VariantDirTestCase(unittest.TestCase): assert not f7.exists() assert f7.rexists() - r = f7.rfile().path + r = f7.rfile().get_internal_path() expect = os.path.normpath(test.workpath('rep1/build/var1/test2.out')) assert r == expect, (repr(r), repr(expect)) assert not f8.exists() assert f8.rexists() - assert f8.rfile().path == os.path.normpath(test.workpath('rep1/build/var2/test2.out')),\ - f8.rfile().path + assert f8.rfile().get_internal_path() == os.path.normpath(test.workpath('rep1/build/var2/test2.out')),\ + f8.rfile().get_internal_path() # Verify the Mkdir and Link actions are called d9 = fs.Dir('build/var2/new_dir') @@ -319,9 +319,9 @@ class VariantDirTestCase(unittest.TestCase): d9.reset_executor() f9.exists() expect = os.path.join('build', 'var2', 'new_dir') - assert dir_made[0].path == expect, dir_made[0].path + assert dir_made[0].get_internal_path() == expect, dir_made[0].get_internal_path() expect = os.path.join('build', 'var2', 'new_dir', 'test9.out') - assert link_made[0].path == expect, link_made[0].path + assert link_made[0].get_internal_path() == expect, link_made[0].get_internal_path() assert f9.linked finally: SCons.Node.FS.Link = save_Link @@ -338,7 +338,7 @@ class VariantDirTestCase(unittest.TestCase): f11 = fs.File('src/file11') t, m = f11.alter_targets() - bdt = [n.path for n in t] + bdt = [n.get_internal_path() for n in t] var1_file11 = os.path.normpath('build/var1/file11') var2_file11 = os.path.normpath('build/var2/file11') assert bdt == [var1_file11, var2_file11], bdt @@ -346,11 +346,11 @@ class VariantDirTestCase(unittest.TestCase): f12 = fs.File('src/file12') f12.builder = 1 bdt, m = f12.alter_targets() - assert bdt == [], [n.path for n in bdt] + assert bdt == [], [n.get_internal_path() for n in bdt] d13 = fs.Dir('src/new_dir') t, m = d13.alter_targets() - bdt = [n.path for n in t] + bdt = [n.get_internal_path() for n in t] var1_new_dir = os.path.normpath('build/var1/new_dir') var2_new_dir = os.path.normpath('build/var2/new_dir') assert bdt == [var1_new_dir, var2_new_dir], bdt @@ -566,13 +566,13 @@ class VariantDirTestCase(unittest.TestCase): f = dir + '/f' fnode = fs.File(dir + '/f') - dp = dnode.srcnode().path + dp = dnode.srcnode().get_internal_path() expect = os.path.normpath(srcnode_map.get(dir, dir)) if dp != expect: print "Dir `%s' srcnode() `%s' != expected `%s'" % (dir, dp, expect) errors = errors + 1 - fp = fnode.srcnode().path + fp = fnode.srcnode().get_internal_path() expect = os.path.normpath(srcnode_map.get(f, f)) if fp != expect: print "File `%s' srcnode() `%s' != expected `%s'" % (f, fp, expect) @@ -584,14 +584,14 @@ class VariantDirTestCase(unittest.TestCase): fnode = fs.File(dir + '/f') t, m = dnode.alter_targets() - tp = t[0].path + tp = t[0].get_internal_path() expect = os.path.normpath(alter_map.get(dir, dir)) if tp != expect: print "Dir `%s' alter_targets() `%s' != expected `%s'" % (dir, tp, expect) errors = errors + 1 t, m = fnode.alter_targets() - tp = t[0].path + tp = t[0].get_internal_path() expect = os.path.normpath(alter_map.get(f, f)) if tp != expect: print "File `%s' alter_targets() `%s' != expected `%s'" % (f, tp, expect) @@ -698,19 +698,19 @@ class DirNodeInfoTestCase(_tempdirTestCase): def test___init__(self): """Test DirNodeInfo initialization""" ddd = self.fs.Dir('ddd') - ni = SCons.Node.FS.DirNodeInfo(ddd) + ni = SCons.Node.FS.DirNodeInfo() class DirBuildInfoTestCase(_tempdirTestCase): def test___init__(self): """Test DirBuildInfo initialization""" ddd = self.fs.Dir('ddd') - bi = SCons.Node.FS.DirBuildInfo(ddd) + bi = SCons.Node.FS.DirBuildInfo() class FileNodeInfoTestCase(_tempdirTestCase): def test___init__(self): """Test FileNodeInfo initialization""" fff = self.fs.File('fff') - ni = SCons.Node.FS.FileNodeInfo(fff) + ni = SCons.Node.FS.FileNodeInfo() assert isinstance(ni, SCons.Node.FS.FileNodeInfo) def test_update(self): @@ -718,7 +718,7 @@ class FileNodeInfoTestCase(_tempdirTestCase): test = self.test fff = self.fs.File('fff') - ni = SCons.Node.FS.FileNodeInfo(fff) + ni = SCons.Node.FS.FileNodeInfo() test.write('fff', "fff\n") @@ -765,37 +765,41 @@ class FileBuildInfoTestCase(_tempdirTestCase): def test___init__(self): """Test File.BuildInfo initialization""" fff = self.fs.File('fff') - bi = SCons.Node.FS.FileBuildInfo(fff) + bi = SCons.Node.FS.FileBuildInfo() assert bi, bi def test_convert_to_sconsign(self): """Test converting to .sconsign file format""" fff = self.fs.File('fff') - bi = SCons.Node.FS.FileBuildInfo(fff) + bi = SCons.Node.FS.FileBuildInfo() assert hasattr(bi, 'convert_to_sconsign') def test_convert_from_sconsign(self): """Test converting from .sconsign file format""" fff = self.fs.File('fff') - bi = SCons.Node.FS.FileBuildInfo(fff) + bi = SCons.Node.FS.FileBuildInfo() assert hasattr(bi, 'convert_from_sconsign') def test_prepare_dependencies(self): """Test that we have a prepare_dependencies() method""" fff = self.fs.File('fff') - bi = SCons.Node.FS.FileBuildInfo(fff) + bi = SCons.Node.FS.FileBuildInfo() bi.prepare_dependencies() def test_format(self): """Test the format() method""" f1 = self.fs.File('f1') - bi1 = SCons.Node.FS.FileBuildInfo(f1) + bi1 = SCons.Node.FS.FileBuildInfo() - s1sig = SCons.Node.FS.FileNodeInfo(self.fs.File('n1')) + self.fs.File('n1') + self.fs.File('n2') + self.fs.File('n3') + + s1sig = SCons.Node.FS.FileNodeInfo() s1sig.csig = 1 - d1sig = SCons.Node.FS.FileNodeInfo(self.fs.File('n2')) + d1sig = SCons.Node.FS.FileNodeInfo() d1sig.timestamp = 2 - i1sig = SCons.Node.FS.FileNodeInfo(self.fs.File('n3')) + i1sig = SCons.Node.FS.FileNodeInfo() i1sig.size = 3 bi1.bsources = [self.fs.File('s1')] @@ -953,7 +957,7 @@ class FSTestCase(_tempdirTestCase): assert isinstance(f1, SCons.Node.FS.File) d1_f1 = os.path.join('d1', 'f1') - assert f1.path == d1_f1, "f1.path %s != %s" % (f1.path, d1_f1) + assert f1.get_internal_path() == d1_f1, "f1.path %s != %s" % (f1.get_internal_path(), d1_f1) assert str(f1) == d1_f1, "str(f1) %s != %s" % (str(f1), d1_f1) x1 = d1.File('x1') @@ -1045,16 +1049,16 @@ class FSTestCase(_tempdirTestCase): name = os.sep if dir.up() is None: - dir_up_path = dir.path + dir_up_path = dir.get_internal_path() else: - dir_up_path = dir.up().path + dir_up_path = dir.up().get_internal_path() assert dir.name == name, \ "dir.name %s != expected name %s" % \ (dir.name, name) - assert dir.path == path, \ + assert dir.get_internal_path() == path, \ "dir.path %s != expected path %s" % \ - (dir.path, path) + (dir.get_internal_path(), path) assert str(dir) == path, \ "str(dir) %s != expected path %s" % \ (str(dir), path) @@ -1136,9 +1140,9 @@ class FSTestCase(_tempdirTestCase): # Test for a bug in 0.04 that did not like looking up # dirs with a trailing slash on Windows. d=fs.Dir('./') - assert d.path == '.', d.abspath + assert d.get_internal_path() == '.', d.get_abspath() d=fs.Dir('foo/') - assert d.path == 'foo', d.abspath + assert d.get_internal_path() == 'foo', d.get_abspath() # Test for sub-classing of node building. global built_it @@ -1167,50 +1171,50 @@ class FSTestCase(_tempdirTestCase): e1 = fs.Entry("d1") assert e1.__class__.__name__ == 'Dir' - match(e1.path, "d1") - match(e1.dir.path, ".") + match(e1.get_internal_path(), "d1") + match(e1.dir.get_internal_path(), ".") e2 = fs.Entry("d1/f1") assert e2.__class__.__name__ == 'File' - match(e2.path, "d1/f1") - match(e2.dir.path, "d1") + match(e2.get_internal_path(), "d1/f1") + match(e2.dir.get_internal_path(), "d1") e3 = fs.Entry("e3") assert e3.__class__.__name__ == 'Entry' - match(e3.path, "e3") - match(e3.dir.path, ".") + match(e3.get_internal_path(), "e3") + match(e3.dir.get_internal_path(), ".") e4 = fs.Entry("d1/e4") assert e4.__class__.__name__ == 'Entry' - match(e4.path, "d1/e4") - match(e4.dir.path, "d1") + match(e4.get_internal_path(), "d1/e4") + match(e4.dir.get_internal_path(), "d1") e5 = fs.Entry("e3/e5") assert e3.__class__.__name__ == 'Dir' - match(e3.path, "e3") - match(e3.dir.path, ".") + match(e3.get_internal_path(), "e3") + match(e3.dir.get_internal_path(), ".") assert e5.__class__.__name__ == 'Entry' - match(e5.path, "e3/e5") - match(e5.dir.path, "e3") + match(e5.get_internal_path(), "e3/e5") + match(e5.dir.get_internal_path(), "e3") e6 = fs.Dir("d1/e4") assert e6 is e4 assert e4.__class__.__name__ == 'Dir' - match(e4.path, "d1/e4") - match(e4.dir.path, "d1") + match(e4.get_internal_path(), "d1/e4") + match(e4.dir.get_internal_path(), "d1") e7 = fs.File("e3/e5") assert e7 is e5 assert e5.__class__.__name__ == 'File' - match(e5.path, "e3/e5") - match(e5.dir.path, "e3") + match(e5.get_internal_path(), "e3/e5") + match(e5.dir.get_internal_path(), "e3") fs.chdir(fs.Dir('subdir')) f11 = fs.File("f11") - match(f11.path, "subdir/f11") + match(f11.get_internal_path(), "subdir/f11") d12 = fs.Dir("d12") e13 = fs.Entry("subdir/e13") - match(e13.path, "subdir/subdir/e13") + match(e13.get_internal_path(), "subdir/subdir/e13") fs.chdir(fs.Dir('..')) # Test scanning @@ -1220,13 +1224,13 @@ class FSTestCase(_tempdirTestCase): f1.builder.target_scanner = Scanner(xyz) f1.scan() - assert f1.implicit[0].path == "xyz" + assert f1.implicit[0].get_internal_path() == "xyz" f1.implicit = [] f1.scan() assert f1.implicit == [] f1.implicit = None f1.scan() - assert f1.implicit[0].path == "xyz" + assert f1.implicit[0].get_internal_path() == "xyz" # Test underlying scanning functionality in get_found_includes() env = Environment() @@ -1284,9 +1288,9 @@ class FSTestCase(_tempdirTestCase): fs.chdir(fs.Dir('subdir')) # The cwd's path is always "." assert str(fs.getcwd()) == ".", str(fs.getcwd()) - assert fs.getcwd().path == 'subdir', fs.getcwd().path + assert fs.getcwd().get_internal_path() == 'subdir', fs.getcwd().get_internal_path() fs.chdir(fs.Dir('../..')) - assert fs.getcwd().path == test.workdir, fs.getcwd().path + assert fs.getcwd().get_internal_path() == test.workdir, fs.getcwd().get_internal_path() f1 = fs.File(test.workpath("do_i_exist")) assert not f1.exists() @@ -1589,15 +1593,15 @@ class FSTestCase(_tempdirTestCase): assert dir.name == name, \ "dir.name %s != expected name %s" % \ (dir.name, name) - assert dir.path == path, \ + assert dir.get_internal_path() == path, \ "dir.path %s != expected path %s" % \ - (dir.path, path) + (dir.get_internal_path(), path) assert str(dir) == path, \ "str(dir) %s != expected path %s" % \ (str(dir), path) - assert dir.up().path == up_path, \ + assert dir.up().get_internal_path() == up_path, \ "dir.up().path %s != expected parent path %s" % \ - (dir.up().path, up_path) + (dir.up().get_internal_path(), up_path) save_os_path = os.path save_os_sep = os.sep @@ -1691,22 +1695,22 @@ class FSTestCase(_tempdirTestCase): name = path.split(os.sep)[-1] if dir.up() is None: - dir_up_path = dir.path + dir_up_path = dir.get_internal_path() else: - dir_up_path = dir.up().path + dir_up_path = dir.up().get_internal_path() assert dir.name == name, \ "dir.name %s != expected name %s" % \ (dir.name, name) - assert dir.path == path, \ + assert dir.get_internal_path() == path, \ "dir.path %s != expected path %s" % \ - (dir.path, path) + (dir.get_internal_path(), path) assert str(dir) == path, \ "str(dir) %s != expected path %s" % \ (str(dir), path) assert dir_up_path == up_path, \ "dir.up().path %s != expected parent path %s" % \ - (dir.up().path, up_path) + (dir.up().get_internal_path(), up_path) save_os_path = os.path save_os_sep = os.sep @@ -1792,7 +1796,7 @@ class FSTestCase(_tempdirTestCase): d1 = fs.Dir('d1') d2 = d1.Dir('d2') - dirs = os.path.normpath(d2.abspath).split(os.sep) + dirs = os.path.normpath(d2.get_abspath()).split(os.sep) above_path = os.path.join(*['..']*len(dirs) + ['above']) above = d2.Dir(above_path) @@ -1831,9 +1835,9 @@ class FSTestCase(_tempdirTestCase): fs = self.fs if sys.platform not in ('win32',): return - p = fs.Dir(r"\\computername\sharename").abspath + p = fs.Dir(r"\\computername\sharename").get_abspath() assert p == r"\\computername\sharename", p - p = fs.Dir(r"\\\computername\sharename").abspath + p = fs.Dir(r"\\\computername\sharename").get_abspath() assert p == r"\\computername\sharename", p def test_rel_path(self): @@ -1980,7 +1984,7 @@ class DirTestCase(_tempdirTestCase): fs.Dir(os.path.join('ddd', 'd1', 'f4')) fs.Dir(os.path.join('ddd', 'd1', 'f5')) dir.scan() - kids = sorted([x.path for x in dir.children(None)]) + kids = sorted([x.get_internal_path() for x in dir.children(None)]) assert kids == [os.path.join('ddd', 'd1'), os.path.join('ddd', 'f1'), os.path.join('ddd', 'f2'), @@ -2024,12 +2028,12 @@ class DirTestCase(_tempdirTestCase): fs.File(os.path.join('ddd', 'f1')) dir.scan() - kids = sorted([x.path for x in dir.children()]) + kids = sorted([x.get_internal_path() for x in dir.children()]) assert kids == [os.path.join('ddd', 'f1')], kids fs.File(os.path.join('ddd', 'f2')) dir.scan() - kids = sorted([x.path for x in dir.children()]) + kids = sorted([x.get_internal_path() for x in dir.children()]) assert kids == [os.path.join('ddd', 'f1'), os.path.join('ddd', 'f2')], kids @@ -2171,8 +2175,12 @@ class DirTestCase(_tempdirTestCase): """ test = self.test - return_true = lambda: 1 + def return_true(node): + return 1 + SCons.Node._is_derived_map[2] = return_true + SCons.Node._exists_map[5] = return_true + test.subdir('src0') test.write(['src0', 'on-disk-f1'], "src0/on-disk-f1\n") test.write(['src0', 'on-disk-f2'], "src0/on-disk-f2\n") @@ -2184,14 +2192,14 @@ class DirTestCase(_tempdirTestCase): self.fs.VariantDir(bld0, src0, duplicate=0) derived_f = src0.File('derived-f') - derived_f.is_derived = return_true + derived_f._func_is_derived = 2 exists_f = src0.File('exists-f') - exists_f.exists = return_true + exists_f._func_exists = 5 derived_e = src0.Entry('derived-e') - derived_e.is_derived = return_true + derived_e._func_is_derived = 2 exists_e = src0.Entry('exists-e') - exists_e.exists = return_true + exists_e._func_exists = 5 def check(result, expect): result = list(map(str, result)) @@ -2245,14 +2253,14 @@ class DirTestCase(_tempdirTestCase): self.fs.VariantDir(bld1, src1, duplicate=1) derived_f = src1.File('derived-f') - derived_f.is_derived = return_true + derived_f._func_is_derived = 2 exists_f = src1.File('exists-f') - exists_f.exists = return_true + exists_f._func_exists = 5 derived_e = src1.Entry('derived-e') - derived_e.is_derived = return_true + derived_e._func_is_derived = 2 exists_e = src1.Entry('exists-e') - exists_e.exists = return_true + exists_e._func_exists = 5 # First check from the source directory. n = src1.srcdir_find_file('does_not_exist') @@ -2432,7 +2440,7 @@ class FileTestCase(_tempdirTestCase): build_f1 = fs.File('build/f1') assert build_f1.exists(), "%s did not realize that %s exists" % (build_f1, src_f1) - assert os.path.exists(build_f1.abspath), "%s did not get duplicated on disk" % build_f1.abspath + assert os.path.exists(build_f1.get_abspath()), "%s did not get duplicated on disk" % build_f1.get_abspath() test.unlink(['src', 'f1']) src_f1.clear() # so the next exists() call will look on disk again @@ -2441,7 +2449,7 @@ class FileTestCase(_tempdirTestCase): build_f1.clear() build_f1.linked = None assert not build_f1.exists(), "%s did not realize that %s disappeared" % (build_f1, src_f1) - assert not os.path.exists(build_f1.abspath), "%s did not get removed after %s was removed" % (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) @@ -2535,7 +2543,7 @@ class GlobTestCase(_tempdirTestCase): for input, string_expect, node_expect in cases: r = self.fs.Glob(input, **kwargs) if node_expect: - r = sorted(r, key=lambda a: a.path) + r = sorted(r, key=lambda a: a.get_internal_path()) result = [] for n in node_expect: if isinstance(n, str): @@ -2906,8 +2914,14 @@ class RepositoryTestCase(_tempdirTestCase): def test_rdir(self): """Test the Dir.rdir() method""" - return_true = lambda: 1 - return_false = lambda: 0 + def return_true(obj): + return 1 + def return_false(obj): + return 0 + SCons.Node._exists_map[5] = return_true + SCons.Node._exists_map[6] = return_false + SCons.Node._is_derived_map[2] = return_true + SCons.Node._is_derived_map[3] = return_false d1 = self.fs.Dir('d1') d2 = self.fs.Dir('d2') @@ -2931,19 +2945,19 @@ class RepositoryTestCase(_tempdirTestCase): assert r == os.path.join(self.rep3, 'd3'), r e1 = self.fs.Dir('e1') - e1.exists = return_false + e1._func_exists = 6 e2 = self.fs.Dir('e2') - e2.exists = return_false + e2._func_exists = 6 # Make sure we match entries in repositories, # regardless of whether they're derived or not. re1 = self.fs.Entry(os.path.join(self.rep1, 'e1')) - re1.exists = return_true - re1.is_derived = return_true + re1._func_exists = 5 + re1._func_is_derived = 2 re2 = self.fs.Entry(os.path.join(self.rep2, 'e2')) - re2.exists = return_true - re2.is_derived = return_false + re2._func_exists = 5 + re2._func_is_derived = 3 r = e1.rdir() assert r is re1, r @@ -2953,8 +2967,14 @@ class RepositoryTestCase(_tempdirTestCase): def test_rfile(self): """Test the File.rfile() method""" - return_true = lambda: 1 - return_false = lambda: 0 + def return_true(obj): + return 1 + def return_false(obj): + return 0 + SCons.Node._exists_map[5] = return_true + SCons.Node._exists_map[6] = return_false + SCons.Node._is_derived_map[2] = return_true + SCons.Node._is_derived_map[3] = return_false f1 = self.fs.File('f1') f2 = self.fs.File('f2') @@ -2978,19 +2998,19 @@ class RepositoryTestCase(_tempdirTestCase): assert r == os.path.join(self.rep3, 'f3'), r e1 = self.fs.File('e1') - e1.exists = return_false + e1._func_exists = 6 e2 = self.fs.File('e2') - e2.exists = return_false + e2._func_exists = 6 # Make sure we match entries in repositories, # regardless of whether they're derived or not. re1 = self.fs.Entry(os.path.join(self.rep1, 'e1')) - re1.exists = return_true - re1.is_derived = return_true + re1._func_exists = 5 + re1._func_is_derived = 2 re2 = self.fs.Entry(os.path.join(self.rep2, 'e2')) - re2.exists = return_true - re2.is_derived = return_false + re2._func_exists = 5 + re2._func_is_derived = 3 r = e1.rfile() assert r is re1, r @@ -3244,9 +3264,13 @@ class stored_infoTestCase(unittest.TestCase): self.xyzzy = 7 def get_entry(self, name): return self.Null() + + def test_sconsign(node): + return MySConsign() f = fs.File('file2', d) - f.dir.sconsign = MySConsign + SCons.Node.FS._sconsign_map[2] = test_sconsign + f.dir._func_sconsign = 2 bi = f.get_stored_info() assert bi.xyzzy == 7, bi @@ -3360,7 +3384,7 @@ class prepareTestCase(unittest.TestCase): xyz.set_state(0) xyz.prepare() - assert dir_made[0].path == "new_dir", dir_made[0] + assert dir_made[0].get_internal_path() == "new_dir", dir_made[0] dir = fs.Dir("dir") dir.prepare() @@ -3373,7 +3397,7 @@ class SConstruct_dirTestCase(unittest.TestCase): fs = SCons.Node.FS.FS() fs.set_SConstruct_dir(fs.Dir('xxx')) - assert fs.SConstruct_dir.path == 'xxx' + assert fs.SConstruct_dir.get_internal_path() == 'xxx' @@ -3552,7 +3576,7 @@ class SpecialAttrTestCase(unittest.TestCase): for_sig = f.suffix.for_signature() assert for_sig == 'baz.blat_suffix', for_sig - s = str(f.abspath) + s = str(f.get_abspath()) assert s == test.workpath('work', 'foo', 'bar', 'baz.blat'), s assert f.abspath.is_literal(), f.abspath for_sig = f.abspath.for_signature() diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index da502b0..a6471b4 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -184,7 +184,7 @@ class Scanner(object): called = None def __call__(self, node): self.called = 1 - return node.found_includes + return node.GetTag('found_includes') def path(self, env, dir, target=None, source=None): return () def select(self, node): @@ -200,7 +200,7 @@ class MyNode(SCons.Node.Node): def __init__(self, name): SCons.Node.Node.__init__(self) self.name = name - self.found_includes = [] + self.Tag('found_includes', []) def __str__(self): return self.name def get_found_includes(self, env, scanner, target): @@ -224,11 +224,18 @@ class Calculator(object): class NodeInfoBaseTestCase(unittest.TestCase): + # The abstract class NodeInfoBase has not enough default slots to perform + # the merge and format test (arbitrary attributes do not work). Do it with a + # derived class that does provide the slots. def test_merge(self): """Test merging NodeInfoBase attributes""" - ni1 = SCons.Node.NodeInfoBase(SCons.Node.Node()) - ni2 = SCons.Node.NodeInfoBase(SCons.Node.Node()) + + class TestNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('a1', 'a2', 'a3') + + ni1 = TestNodeInfo() + ni2 = TestNodeInfo() ni1.a1 = 1 ni1.a2 = 2 @@ -237,27 +244,32 @@ class NodeInfoBaseTestCase(unittest.TestCase): ni2.a3 = 333 ni1.merge(ni2) - expect = {'a1':1, 'a2':222, 'a3':333, '_version_id':1} - assert ni1.__dict__ == expect, ni1.__dict__ + assert ni1.a1 == 1, ni1.a1 + assert ni1.a2 == 222, ni1.a2 + assert ni1.a3 == 333, ni1.a3 def test_update(self): """Test the update() method""" - ni = SCons.Node.NodeInfoBase(SCons.Node.Node()) + ni = SCons.Node.NodeInfoBase() ni.update(SCons.Node.Node()) def test_format(self): """Test the NodeInfoBase.format() method""" - ni1 = SCons.Node.NodeInfoBase(SCons.Node.Node()) + + class TestNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('xxx', 'yyy', 'zzz') + + ni1 = TestNodeInfo() ni1.xxx = 'x' ni1.yyy = 'y' ni1.zzz = 'z' f = ni1.format() - assert f == ['1', 'x', 'y', 'z'], f + assert f == ['x', 'y', 'z'], f + + field_list = ['xxx', 'zzz', 'aaa'] - ni1.field_list = ['xxx', 'zzz', 'aaa'] - - f = ni1.format() + f = ni1.format(field_list) assert f == ['x', 'z', 'None'], f @@ -267,26 +279,26 @@ class BuildInfoBaseTestCase(unittest.TestCase): def test___init__(self): """Test BuildInfoBase initialization""" n = SCons.Node.Node() - bi = SCons.Node.BuildInfoBase(n) + bi = SCons.Node.BuildInfoBase() assert bi def test_merge(self): """Test merging BuildInfoBase attributes""" n1 = SCons.Node.Node() - bi1 = SCons.Node.BuildInfoBase(n1) + bi1 = SCons.Node.BuildInfoBase() n2 = SCons.Node.Node() - bi2 = SCons.Node.BuildInfoBase(n2) + bi2 = SCons.Node.BuildInfoBase() - bi1.a1 = 1 - bi1.a2 = 2 + bi1.bsources = 1 + bi1.bdepends = 2 - bi2.a2 = 222 - bi2.a3 = 333 + bi2.bdepends = 222 + bi2.bact = 333 bi1.merge(bi2) - assert bi1.a1 == 1, bi1.a1 - assert bi1.a2 == 222, bi1.a2 - assert bi1.a3 == 333, bi1.a3 + assert bi1.bsources == 1, bi1.bsources + assert bi1.bdepends == 222, bi1.bdepends + assert bi1.bact == 333, bi1.bact class NodeTestCase(unittest.TestCase): @@ -427,6 +439,7 @@ class NodeTestCase(unittest.TestCase): def test_built(self): """Test the built() method""" class SubNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('updated',) def update(self, node): self.updated = 1 class SubNode(SCons.Node.Node): @@ -434,7 +447,7 @@ class NodeTestCase(unittest.TestCase): self.cleared = 1 n = SubNode() - n.ninfo = SubNodeInfo(n) + n.ninfo = SubNodeInfo() n.built() assert n.cleared, n.cleared assert n.ninfo.updated, n.ninfo.cleared @@ -568,32 +581,56 @@ class NodeTestCase(unittest.TestCase): def test_get_csig(self): """Test generic content signature calculation """ - node = SCons.Node.Node() - node.get_contents = lambda: 444 - result = node.get_csig() - assert result == '550a141f12de6341fba65b0ad0433500', result + + class TestNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('csig',) + try: + SCons.Node.Node.NodeInfo = TestNodeInfo + def my_contents(obj): + return 444 + SCons.Node._get_contents_map[4] = my_contents + node = SCons.Node.Node() + node._func_get_contents = 4 + result = node.get_csig() + assert result == '550a141f12de6341fba65b0ad0433500', result + finally: + SCons.Node.Node.NodeInfo = SCons.Node.NodeInfoBase def test_get_cachedir_csig(self): """Test content signature calculation for CacheDir """ - node = SCons.Node.Node() - node.get_contents = lambda: 555 - result = node.get_cachedir_csig() - assert result == '15de21c670ae7c3f6f3f1f37029303c9', result + class TestNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('csig',) + try: + SCons.Node.Node.NodeInfo = TestNodeInfo + def my_contents(obj): + return 555 + SCons.Node._get_contents_map[4] = my_contents + node = SCons.Node.Node() + node._func_get_contents = 4 + result = node.get_cachedir_csig() + assert result == '15de21c670ae7c3f6f3f1f37029303c9', result + finally: + SCons.Node.Node.NodeInfo = SCons.Node.NodeInfoBase def test_get_binfo(self): """Test fetching/creating a build information structure """ + class TestNodeInfo(SCons.Node.NodeInfoBase): + __slots__ = ('csig',) + SCons.Node.Node.NodeInfo = TestNodeInfo node = SCons.Node.Node() - + binfo = node.get_binfo() assert isinstance(binfo, SCons.Node.BuildInfoBase), binfo node = SCons.Node.Node() d = SCons.Node.Node() - d.get_ninfo().csig = 777 + ninfo = d.get_ninfo() + assert isinstance(ninfo, SCons.Node.NodeInfoBase), ninfo i = SCons.Node.Node() - i.get_ninfo().csig = 888 + ninfo = i.get_ninfo() + assert isinstance(ninfo, SCons.Node.NodeInfoBase), ninfo node.depends = [d] node.implicit = [i] @@ -655,7 +692,7 @@ class NodeTestCase(unittest.TestCase): """Test calling the method to store build information """ node = SCons.Node.Node() - node.store_info() + SCons.Node.store_info_map[node.store_info](node) def test_get_stored_info(self): """Test calling the method to fetch stored build information @@ -888,7 +925,7 @@ class NodeTestCase(unittest.TestCase): s = Scanner() d1 = MyNode("d1") d2 = MyNode("d2") - node.found_includes = [d1, d2] + node.Tag('found_includes', [d1, d2]) # Simple return of the found includes deps = node.get_implicit_deps(env, s, target) @@ -898,14 +935,14 @@ class NodeTestCase(unittest.TestCase): e = MyNode("eee") f = MyNode("fff") g = MyNode("ggg") - d1.found_includes = [e, f] - d2.found_includes = [e, f] - f.found_includes = [g] + d1.Tag('found_includes', [e, f]) + d2.Tag('found_includes', [e, f]) + f.Tag('found_includes', [g]) deps = node.get_implicit_deps(env, s, target) assert deps == [d1, d2, e, f, g], list(map(str, deps)) # Recursive scanning eliminates duplicates - e.found_includes = [f] + e.Tag('found_includes', [f]) deps = node.get_implicit_deps(env, s, target) assert deps == [d1, d2, e, f, g], list(map(str, deps)) @@ -994,7 +1031,7 @@ class NodeTestCase(unittest.TestCase): s = Scanner() d = MyNode("ddd") - node.found_includes = [d] + node.Tag('found_includes', [d]) node.builder.target_scanner = s assert node.implicit is None @@ -1207,7 +1244,7 @@ class NodeTestCase(unittest.TestCase): def test_Annotate(self): """Test using an interface-specific Annotate function.""" def my_annotate(node, self=self): - node.annotation = self.node_string + node.Tag('annotation', self.node_string) save_Annotate = SCons.Node.Annotate SCons.Node.Annotate = my_annotate @@ -1215,11 +1252,13 @@ class NodeTestCase(unittest.TestCase): try: self.node_string = '#1' n = SCons.Node.Node() - assert n.annotation == '#1', n.annotation + a = n.GetTag('annotation') + assert a == '#1', a self.node_string = '#2' n = SCons.Node.Node() - assert n.annotation == '#2', n.annotation + a = n.GetTag('annotation') + assert a == '#2', a finally: SCons.Node.Annotate = save_Annotate @@ -1230,7 +1269,7 @@ class NodeTestCase(unittest.TestCase): n.set_state(3) n.binfo = 'xyz' n.includes = 'testincludes' - n.found_include = {'testkey':'testvalue'} + n.Tag('found_includes', {'testkey':'testvalue'}) n.implicit = 'testimplicit' x = MyExecutor() diff --git a/src/engine/SCons/Node/Python.py b/src/engine/SCons/Node/Python.py index 3d8bdaa..f151fc5 100644 --- a/src/engine/SCons/Node/Python.py +++ b/src/engine/SCons/Node/Python.py @@ -32,15 +32,49 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import SCons.Node class ValueNodeInfo(SCons.Node.NodeInfoBase): - current_version_id = 1 + __slots__ = ('csig',) + current_version_id = 2 field_list = ['csig'] def str_to_node(self, s): return Value(s) + def __getstate__(self): + """ + Return all fields that shall be pickled. Walk the slots in the class + hierarchy and add those to the state dictionary. If a '__dict__' slot is + available, copy all entries to the dictionary. Also include the version + id, which is fixed for all instances of a class. + """ + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + + return state + + def __setstate__(self, state): + """ + Restore the attributes from a pickled state. + """ + # TODO check or discard version + del state['_version_id'] + for key, value in state.items(): + if key not in ('__weakref__',): + setattr(self, key, value) + + class ValueBuildInfo(SCons.Node.BuildInfoBase): - current_version_id = 1 + __slots__ = () + current_version_id = 2 class Value(SCons.Node.Node): """A class for Python variables, typically passed on the command line @@ -53,6 +87,8 @@ class Value(SCons.Node.Node): def __init__(self, value, built_value=None): SCons.Node.Node.__init__(self) self.value = value + self.changed_since_last_build = 6 + self.store_info = 0 if built_value is not None: self.built_value = built_value diff --git a/src/engine/SCons/Node/PythonTests.py b/src/engine/SCons/Node/PythonTests.py index fcdfe77..e2e36bf 100644 --- a/src/engine/SCons/Node/PythonTests.py +++ b/src/engine/SCons/Node/PythonTests.py @@ -104,13 +104,13 @@ class ValueNodeInfoTestCase(unittest.TestCase): def test___init__(self): """Test ValueNodeInfo initialization""" vvv = SCons.Node.Python.Value('vvv') - ni = SCons.Node.Python.ValueNodeInfo(vvv) + ni = SCons.Node.Python.ValueNodeInfo() class ValueBuildInfoTestCase(unittest.TestCase): def test___init__(self): """Test ValueBuildInfo initialization""" vvv = SCons.Node.Python.Value('vvv') - bi = SCons.Node.Python.ValueBuildInfo(vvv) + bi = SCons.Node.Python.ValueBuildInfo() if __name__ == "__main__": suite = unittest.TestSuite() diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 1f62971..f2d37c2 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -55,6 +55,8 @@ import SCons.Util from SCons.Debug import Trace +print_duplicate = 0 + def classname(obj): return str(obj.__class__).split('.')[-1] @@ -105,6 +107,233 @@ Annotate = do_nothing # clean builds and update runs (see release_target_info). interactive = False +def is_derived_none(node): + raise NotImplementedError + +def is_derived_node(node): + """ + Returns true if this node is derived (i.e. built). + """ + return node.has_builder() or node.side_effect + +_is_derived_map = {0 : is_derived_none, + 1 : is_derived_node} + +def exists_none(node): + raise NotImplementedError + +def exists_always(node): + return 1 + +def exists_base(node): + return node.stat() is not None + +def exists_entry(node): + """Return if the Entry exists. Check the file system to see + what we should turn into first. Assume a file if there's no + directory.""" + 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: + src = node.srcnode() + if src is not node: + # At this point, src is meant to be copied in a variant directory. + src = src.rfile() + if src.get_abspath() != node.get_abspath(): + if src.exists(): + node.do_duplicate(src) + # Can't return 1 here because the duplication might + # 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 variant directory. + if print_duplicate: + print "dup: no src for %s, unlinking old variant copy"%self + if exists_base(node) or node.islink(): + node.fs.unlink(node.get_internal_path()) + # Return None explicitly because the Base.exists() call + # above will have cached its value if the file existed. + return None + return exists_base(node) + +_exists_map = {0 : exists_none, + 1 : exists_always, + 2 : exists_base, + 3 : exists_entry, + 4 : exists_file} + + +def rexists_none(node): + raise NotImplementedError + +def rexists_node(node): + return node.exists() + +def rexists_base(node): + return node.rfile().exists() + +_rexists_map = {0 : rexists_none, + 1 : rexists_node, + 2 : rexists_base} + +def get_contents_none(node): + raise NotImplementedError + +def get_contents_entry(node): + """Fetch the contents of the entry. Returns the exact binary + contents of the file.""" + try: + node = node.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_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 _get_contents_map[node._func_get_contents](node) + +def get_contents_dir(node): + """Return content signatures and names of all our children + separated by new-lines. Ensure that the nodes are sorted.""" + contents = [] + for n in sorted(node.children(), key=lambda t: t.name): + contents.append('%s %s\n' % (n.get_csig(), n.name)) + return ''.join(contents) + +def get_contents_file(node): + if not node.rexists(): + return '' + fname = node.rfile().get_abspath() + try: + contents = open(fname, "rb").read() + except EnvironmentError, e: + if not e.filename: + e.filename = fname + raise + return contents + +_get_contents_map = {0 : get_contents_none, + 1 : get_contents_entry, + 2 : get_contents_dir, + 3 : get_contents_file} + +def target_from_source_none(node, prefix, suffix, splitext): + raise NotImplementedError + +def target_from_source_base(node, prefix, suffix, splitext): + return node.dir.Entry(prefix + splitext(node.name)[0] + suffix) + +_target_from_source_map = {0 : target_from_source_none, + 1 : target_from_source_base} + +# +# The new decider subsystem for Nodes +# +# We would set and overwrite the changed_since_last_build function +# before, but for being able to use slots (less memory!) we now have +# a dictionary of the different decider functions. Then in the Node +# subclasses we simply store the index to the decider that should be +# used by it. +# + +# +# First, the single decider functions +# +def changed_since_last_build_node(node, target, prev_ni): + """ + + Must be overridden in a specific subclass to return True if this + Node (a dependency) has changed since the last time it was used + to build the specified target. prev_ni is this Node's state (for + example, its file timestamp, length, maybe content signature) + as of the last time the target was built. + + Note that this method is called through the dependency, not the + target, because a dependency Node must be able to use its own + logic to decide if it changed. For example, File Nodes need to + obey if we're configured to use timestamps, but Python Value Nodes + never use timestamps and always use the content. If this method + were called through the target, then each Node's implementation + of this method would have to have more complicated logic to + handle all the different Node types on which it might depend. + """ + raise NotImplementedError + +def changed_since_last_build_alias(node, target, prev_ni): + cur_csig = node.get_csig() + try: + return cur_csig != prev_ni.csig + 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) + +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: + return cur_csig != prev_ni.csig + except AttributeError: + return 1 + + +# +# Now, the mapping from indices to decider functions +# +_decider_map = {0 : changed_since_last_build_node, + 1 : changed_since_last_build_alias, + 2 : changed_since_last_build_entry, + 3 : changed_since_last_build_state_changed, + 4 : decide_source, + 5 : decide_target, + 6 : changed_since_last_build_python} + +do_store_info = True + +# +# The new store_info subsystem for Nodes +# +# We would set and overwrite the store_info function +# before, but for being able to use slots (less memory!) we now have +# a dictionary of the different functions. Then in the Node +# subclasses we simply store the index to the info method that should be +# used by it. +# + +# +# First, the single info functions +# + +def store_info_pass(node): + pass + +def store_info_file(node): + # Merge our build information into the already-stored entry. + # This accommodates "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. + if do_store_info: + node.dir.sconsign().store_info(node.name, node) + + +store_info_map = {0 : store_info_pass, + 1 : store_info_file} + # Classes for signature info for Nodes. class NodeInfoBase(object): @@ -114,11 +343,8 @@ class NodeInfoBase(object): Node subclasses should subclass NodeInfoBase to provide their own logic for dealing with their own Node-specific signature information. """ - current_version_id = 1 - def __init__(self, node=None): - # Create an object attribute from the class attribute so it ends up - # in the pickled data in the .sconsign file. - self._version_id = self.current_version_id + __slots__ = ('__weakref__',) + current_version_id = 2 def update(self, node): try: field_list = self.field_list @@ -138,13 +364,25 @@ class NodeInfoBase(object): def convert(self, node, val): pass def merge(self, other): - self.__dict__.update(other.__dict__) + """ + Merge the fields of another object into this object. Already existing + information is overwritten by the other instance's data. + WARNING: If a '__dict__' slot is added, it should be updated instead of + replaced. + """ + state = other.__getstate__() + self.__setstate__(state) def format(self, field_list=None, names=0): if field_list is None: try: field_list = self.field_list except AttributeError: - field_list = sorted(self.__dict__.keys()) + field_list = getattr(self, '__dict__', {}).keys() + for obj in type(self).mro(): + for slot in getattr(obj, '__slots__', ()): + if slot not in ('__weakref__', '__dict__'): + field_list.append(slot) + field_list.sort() fields = [] for field in field_list: try: @@ -157,6 +395,38 @@ class NodeInfoBase(object): fields.append(f) return fields + def __getstate__(self): + """ + Return all fields that shall be pickled. Walk the slots in the class + hierarchy and add those to the state dictionary. If a '__dict__' slot is + available, copy all entries to the dictionary. Also include the version + id, which is fixed for all instances of a class. + """ + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + return state + + def __setstate__(self, state): + """ + Restore the attributes from a pickled state. The version is discarded. + """ + # TODO check or discard version + del state['_version_id'] + + for key, value in state.items(): + if key not in ('__weakref__',): + setattr(self, key, value) + + class BuildInfoBase(object): """ The generic base class for build information for a Node. @@ -167,30 +437,106 @@ class BuildInfoBase(object): generic build stuff we have to track: sources, explicit dependencies, implicit dependencies, and action information. """ - current_version_id = 1 - def __init__(self, node=None): + __slots__ = ("bsourcesigs", "bdependsigs", "bimplicitsigs", "bactsig", + "bsources", "bdepends", "bact", "bimplicit", "__weakref__") + current_version_id = 2 + def __init__(self): # Create an object attribute from the class attribute so it ends up # in the pickled data in the .sconsign file. - self._version_id = self.current_version_id self.bsourcesigs = [] self.bdependsigs = [] self.bimplicitsigs = [] self.bactsig = None def merge(self, other): - self.__dict__.update(other.__dict__) + """ + Merge the fields of another object into this object. Already existing + information is overwritten by the other instance's data. + WARNING: If a '__dict__' slot is added, it should be updated instead of + replaced. + """ + state = other.__getstate__() + self.__setstate__(state) + + def __getstate__(self): + """ + Return all fields that shall be pickled. Walk the slots in the class + hierarchy and add those to the state dictionary. If a '__dict__' slot is + available, copy all entries to the dictionary. Also include the version + id, which is fixed for all instances of a class. + """ + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + return state + + def __setstate__(self, state): + """ + Restore the attributes from a pickled state. + """ + # TODO check or discard version + del state['_version_id'] + for key, value in state.items(): + if key not in ('__weakref__',): + setattr(self, key, value) class Node(object): """The base Node class, for entities that we know how to build, or use to build other Nodes. """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] + __slots__ = ['sources', + 'sources_set', + '_specific_sources', + 'depends', + 'depends_set', + 'ignore', + 'ignore_set', + 'prerequisites', + 'implicit', + 'waiting_parents', + 'waiting_s_e', + 'ref_count', + 'wkids', + 'env', + 'state', + 'precious', + 'noclean', + 'nocache', + 'cached', + 'always_build', + 'includes', + 'attributes', + 'side_effect', + 'side_effects', + 'linked', + '_memo', + 'executor', + 'binfo', + 'ninfo', + 'builder', + 'is_explicit', + 'implicit_set', + 'changed_since_last_build', + 'store_info', + 'pseudo', + '_tags', + '_func_is_derived', + '_func_exists', + '_func_rexists', + '_func_get_contents', + '_func_target_from_source'] class Attrs(object): - pass + __slots__ = ('shared', '__dict__') + def __init__(self): if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.Node') @@ -234,7 +580,15 @@ class Node(object): self.side_effect = 0 # true iff this node is a side effect self.side_effects = [] # the side effects of building this target self.linked = 0 # is this node linked to the variant directory? - + self.changed_since_last_build = 0 + self.store_info = 0 + self._tags = None + self._func_is_derived = 1 + self._func_exists = 1 + self._func_rexists = 1 + self._func_get_contents = 0 + self._func_target_from_source = 0 + self.clear_memoized_values() # Let the interface in which the build engine is embedded @@ -248,8 +602,7 @@ class Node(object): def get_suffix(self): return '' - memoizer_counters.append(SCons.Memoize.CountValue('get_build_env')) - + @SCons.Memoize.CountMethodCall def get_build_env(self): """Fetch the appropriate Environment to build this node. """ @@ -418,7 +771,7 @@ class Node(object): pass else: self.ninfo.update(self) - self.store_info() + SCons.Node.store_info_map[self.store_info](self) def release_target_info(self): """Called just after this node has been marked @@ -546,7 +899,7 @@ class Node(object): example: source with source builders are not derived in this sense, and hence should not return true. """ - return self.has_builder() or self.side_effect + return _is_derived_map[self._func_is_derived](self) def alter_targets(self): """Return a list of alternate targets for this Node. @@ -706,7 +1059,7 @@ class Node(object): BuildInfo = BuildInfoBase def new_ninfo(self): - ninfo = self.NodeInfo(self) + ninfo = self.NodeInfo() return ninfo def get_ninfo(self): @@ -717,7 +1070,7 @@ class Node(object): return self.ninfo def new_binfo(self): - binfo = self.BuildInfo(self) + binfo = self.BuildInfo() return binfo def get_binfo(self): @@ -802,14 +1155,6 @@ class Node(object): def get_cachedir_csig(self): return self.get_csig() - def store_info(self): - """Make the build signature permanent (that is, store it in the - .sconsign file or equivalent).""" - pass - - def do_not_store_info(self): - pass - def get_stored_info(self): return None @@ -847,13 +1192,16 @@ class Node(object): def exists(self): """Does this node exists?""" - # All node exist by default: - return 1 + return _exists_map[self._func_exists](self) def rexists(self): """Does this node exist locally or in a repositiory?""" # There are no repositories by default: - return self.exists() + return _rexists_map[self._func_rexists](self) + + def get_contents(self): + """Fetch the contents of the entry.""" + return _get_contents_map[self._func_get_contents](self) def missing(self): return not self.is_derived() and \ @@ -941,11 +1289,10 @@ class Node(object): # build info that it's cached so we can re-calculate it. self.executor_cleanup() - memoizer_counters.append(SCons.Memoize.CountValue('_children_get')) - + @SCons.Memoize.CountMethodCall def _children_get(self): try: - return self._memo['children_get'] + return self._memo['_children_get'] except KeyError: pass @@ -976,7 +1323,7 @@ class Node(object): else: children = self.all_children(scan=0) - self._memo['children_get'] = children + self._memo['_children_get'] = children return children def all_children(self, scan=1): @@ -1016,9 +1363,6 @@ class Node(object): def get_state(self): return self.state - def state_has_changed(self, target, prev_ni): - return (self.state != SCons.Node.up_to_date) - def get_env(self): env = self.env if not env: @@ -1026,28 +1370,28 @@ class Node(object): env = SCons.Defaults.DefaultEnvironment() return env - def changed_since_last_build(self, target, prev_ni): - """ - - Must be overridden in a specific subclass to return True if this - Node (a dependency) has changed since the last time it was used - to build the specified target. prev_ni is this Node's state (for - example, its file timestamp, length, maybe content signature) - as of the last time the target was built. - - Note that this method is called through the dependency, not the - target, because a dependency Node must be able to use its own - logic to decide if it changed. For example, File Nodes need to - obey if we're configured to use timestamps, but Python Value Nodes - never use timestamps and always use the content. If this method - were called through the target, then each Node's implementation - of this method would have to have more complicated logic to - handle all the different Node types on which it might depend. - """ - raise NotImplementedError - def Decider(self, function): - SCons.Util.AddMethod(self, function, 'changed_since_last_build') + foundkey = None + for k, v in _decider_map.iteritems(): + if v == function: + foundkey = k + break + if not foundkey: + foundkey = len(_decider_map) + _decider_map[foundkey] = function + self.changed_since_last_build = foundkey + + def Tag(self, key, value): + """ Add a user-defined tag. """ + if not self._tags: + self._tags = {} + self._tags[key] = value + + def GetTag(self, key): + """ Return a user-defined tag. """ + if not self._tags: + return None + return self._tags.get(key, None) def changed(self, node=None, allowcache=False): """ @@ -1095,7 +1439,7 @@ class Node(object): result = True for child, prev_ni in zip(children, then): - if child.changed_since_last_build(self, prev_ni): + if _decider_map[child.changed_since_last_build](child, self, prev_ni): if t: Trace(': %s changed' % child) result = True @@ -1266,7 +1610,7 @@ class Node(object): for k in new_bkids: if not k in old_bkids: lines.append("`%s' is a new dependency\n" % stringify(k)) - elif k.changed_since_last_build(self, osig[k]): + elif _decider_map[k.changed_since_last_build](k, self, osig[k]): lines.append("`%s' changed\n" % stringify(k)) if len(lines) == 0 and old_bkids != new_bkids: diff --git a/src/engine/SCons/PathList.py b/src/engine/SCons/PathList.py index f3de57c..350e1ac 100644 --- a/src/engine/SCons/PathList.py +++ b/src/engine/SCons/PathList.py @@ -171,11 +171,6 @@ class PathListCache(object): cheaply avoid re-parsing both values of CPPPATH by using the common value from this cache. """ - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - memoizer_counters = [] - def __init__(self): self._memo = {} @@ -196,8 +191,7 @@ class PathListCache(object): pathlist = tuple(SCons.Util.flatten(pathlist)) return pathlist - memoizer_counters.append(SCons.Memoize.CountDict('PathList', _PathList_key)) - + @SCons.Memoize.CountDictCall(_PathList_key) def PathList(self, pathlist): """ Returns the cached _PathList object for the specified pathlist, diff --git a/src/engine/SCons/Platform/cygwin.py b/src/engine/SCons/Platform/cygwin.py index a012682..34c79ff 100644 --- a/src/engine/SCons/Platform/cygwin.py +++ b/src/engine/SCons/Platform/cygwin.py @@ -42,8 +42,8 @@ def generate(env): env['PROGSUFFIX'] = '.exe' env['SHLIBPREFIX'] = '' env['SHLIBSUFFIX'] = '.dll' - env['LIBPREFIXES'] = [ '$LIBPREFIX', '$SHLIBPREFIX' ] - env['LIBSUFFIXES'] = [ '$LIBSUFFIX', '$SHLIBSUFFIX' ] + env['LIBPREFIXES'] = [ '$LIBPREFIX', '$SHLIBPREFIX', '$IMPLIBPREFIX' ] + env['LIBSUFFIXES'] = [ '$LIBSUFFIX', '$SHLIBSUFFIX', '$IMPLIBSUFFIX' ] env['TEMPFILE'] = TempFileMunge env['TEMPFILEPREFIX'] = '@' env['MAXLINELENGTH'] = 2048 diff --git a/src/engine/SCons/SConf.py b/src/engine/SCons/SConf.py index 87432ef..8bce8ce 100644 --- a/src/engine/SCons/SConf.py +++ b/src/engine/SCons/SConf.py @@ -175,8 +175,11 @@ class SConfBuildInfo(SCons.Node.FS.FileBuildInfo): are result (did the builder succeed last time?) and string, which contains messages of the original build phase. """ - result = None # -> 0/None -> no error, != 0 error - string = None # the stdout / stderr output when building the target + __slots__ = ('result', 'string') + + def __init__(self): + self.result = None # -> 0/None -> no error, != 0 error + self.string = None # the stdout / stderr output when building the target def set_build_result(self, result, string): self.result = result @@ -352,8 +355,10 @@ class SConfBuildTask(SCons.Taskmaster.AlwaysTask): raise SCons.Errors.ExplicitExit(self.targets[0],exc_value.code) except Exception, e: for t in self.targets: - binfo = t.get_binfo() - binfo.__class__ = SConfBuildInfo + #binfo = t.get_binfo() + #binfo.__class__ = SConfBuildInfo + binfo = SConfBuildInfo() + binfo.merge(t.get_binfo()) binfo.set_build_result(1, s.getvalue()) sconsign_entry = SCons.SConsign.SConsignEntry() sconsign_entry.binfo = binfo @@ -370,8 +375,10 @@ class SConfBuildTask(SCons.Taskmaster.AlwaysTask): raise e else: for t in self.targets: - binfo = t.get_binfo() - binfo.__class__ = SConfBuildInfo + #binfo = t.get_binfo() + #binfo.__class__ = SConfBuildInfo + binfo = SConfBuildInfo() + binfo.merge(t.get_binfo()) binfo.set_build_result(0, s.getvalue()) sconsign_entry = SCons.SConsign.SConsignEntry() sconsign_entry.binfo = binfo @@ -500,7 +507,7 @@ class SConfBase(object): # we override the store_info() method with a null place-holder # so we really control how it gets written. for n in nodes: - n.store_info = n.do_not_store_info + n.store_info = 0 if not hasattr(n, 'attributes'): n.attributes = SCons.Node.Node.Attrs() n.attributes.keep_targetinfo = 1 @@ -640,7 +647,7 @@ class SConfBase(object): ok = self.TryLink(text, extension) if( ok ): prog = self.lastTarget - pname = prog.path + pname = prog.get_internal_path() output = self.confdir.File(os.path.basename(pname)+'.out') node = self.env.Command(output, prog, [ [ pname, ">", "${TARGET}"] ]) ok = self.BuildNodes(node) @@ -684,7 +691,6 @@ class SConfBase(object): else: if not os.path.isdir( dirName ): os.makedirs( dirName ) - node._exists = 1 def _startup(self): """Private method. Set up logstream, and set the environment diff --git a/src/engine/SCons/SConfTests.py b/src/engine/SCons/SConfTests.py index 1c4b401..233ee78 100644 --- a/src/engine/SCons/SConfTests.py +++ b/src/engine/SCons/SConfTests.py @@ -221,8 +221,6 @@ class SConfTestCase(unittest.TestCase): pass def get_stored_info(self): pass - def do_not_store_info(self): - pass def get_executor(self): class Executor(object): def __init__(self, targets): diff --git a/src/engine/SCons/SConsign.py b/src/engine/SCons/SConsign.py index 6555fcb..74ee804 100644 --- a/src/engine/SCons/SConsign.py +++ b/src/engine/SCons/SConsign.py @@ -122,16 +122,40 @@ class SConsignEntry(object): XXX As coded below, we do expect a '.binfo' attribute to be added, but we'll probably generalize this in the next refactorings. """ - current_version_id = 1 + __slots__ = ("binfo", "ninfo", "__weakref__") + current_version_id = 2 + def __init__(self): # Create an object attribute from the class attribute so it ends up # in the pickled data in the .sconsign file. - _version_id = self.current_version_id + #_version_id = self.current_version_id + pass + def convert_to_sconsign(self): self.binfo.convert_to_sconsign() + def convert_from_sconsign(self, dir, name): self.binfo.convert_from_sconsign(dir, name) + def __getstate__(self): + state = getattr(self, '__dict__', {}).copy() + for obj in type(self).mro(): + for name in getattr(obj,'__slots__',()): + if hasattr(self, name): + state[name] = getattr(self, name) + + state['_version_id'] = self.current_version_id + try: + del state['__weakref__'] + except KeyError: + pass + return state + + def __setstate__(self, state): + for key, value in state.items(): + if key not in ('_version_id','__weakref__'): + setattr(self, key, value) + class Base(object): """ This is the controlling class for the signatures for the collection of @@ -202,7 +226,7 @@ class DB(Base): # Read using the path relative to the top of the Repository # (self.dir.tpath) from which we're fetching the signature # information. - path = normcase(dir.tpath) + path = normcase(dir.get_tpath()) try: rawentries = db[path] except KeyError: @@ -217,7 +241,7 @@ class DB(Base): raise except Exception, e: SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning, - "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e)) + "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.get_tpath(), e)) for key, entry in self.entries.items(): entry.convert_from_sconsign(dir, key) @@ -244,7 +268,7 @@ class DB(Base): # directory (self.dir.path), not relative to the top of # the Repository; we only write to our own .sconsign file, # not to .sconsign files in Repositories. - path = normcase(self.dir.path) + path = normcase(self.dir.get_internal_path()) for key, entry in self.entries.items(): entry.convert_to_sconsign() db[path] = pickle.dumps(self.entries, 1) @@ -287,7 +311,7 @@ class DirFile(Dir): """ self.dir = dir - self.sconsign = os.path.join(dir.path, '.sconsign') + self.sconsign = os.path.join(dir.get_internal_path(), '.sconsign') try: fp = open(self.sconsign, 'rb') @@ -323,7 +347,7 @@ class DirFile(Dir): self.merge() - temp = os.path.join(self.dir.path, '.scons%d' % os.getpid()) + temp = os.path.join(self.dir.get_internal_path(), '.scons%d' % os.getpid()) try: file = open(temp, 'wb') fname = temp diff --git a/src/engine/SCons/SConsignTests.py b/src/engine/SCons/SConsignTests.py index f71e53e..d40a7b6 100644 --- a/src/engine/SCons/SConsignTests.py +++ b/src/engine/SCons/SConsignTests.py @@ -62,6 +62,10 @@ class DummyNode(object): return self.binfo def get_binfo(self): return self.binfo + def get_internal_path(self): + return self.path + def get_tpath(self): + return self.tpath class SConsignTestCase(unittest.TestCase): def setUp(self): diff --git a/src/engine/SCons/Scanner/CTests.py b/src/engine/SCons/Scanner/CTests.py index 6418754..9c7df12 100644 --- a/src/engine/SCons/Scanner/CTests.py +++ b/src/engine/SCons/Scanner/CTests.py @@ -270,17 +270,18 @@ class CScannerTestCase5(unittest.TestCase): path = s.path(env) n = env.File('f3.cpp') - def my_rexists(s=n): - s.rexists_called = 1 - return s.old_rexists() - setattr(n, 'old_rexists', n.rexists) - setattr(n, 'rexists', my_rexists) + def my_rexists(s): + s.Tag('rexists_called', 1) + return SCons.Node._rexists_map[s.GetTag('old_rexists')](s) + n.Tag('old_rexists', n._func_rexists) + SCons.Node._rexists_map[3] = my_rexists + n._func_rexists = 3 deps = s(n, env, path) # Make sure rexists() got called on the file node being # scanned, essential for cooperation with VariantDir functionality. - assert n.rexists_called + assert n.GetTag('rexists_called') headers = ['f1.h', 'f2.h', 'f3-test.h', 'd1/f1.h', 'd1/f2.h', 'd1/f3-test.h'] diff --git a/src/engine/SCons/Scanner/Dir.py b/src/engine/SCons/Scanner/Dir.py index 1cecfb7..cbfb6fb 100644 --- a/src/engine/SCons/Scanner/Dir.py +++ b/src/engine/SCons/Scanner/Dir.py @@ -77,7 +77,7 @@ def scan_on_disk(node, env, path=()): that and then call the in-memory scanning function. """ try: - flist = node.fs.listdir(node.abspath) + flist = node.fs.listdir(node.get_abspath()) except (IOError, OSError): return [] e = node.Entry diff --git a/src/engine/SCons/Scanner/FortranTests.py b/src/engine/SCons/Scanner/FortranTests.py index 252da64..aaefa79 100644 --- a/src/engine/SCons/Scanner/FortranTests.py +++ b/src/engine/SCons/Scanner/FortranTests.py @@ -356,17 +356,18 @@ class FortranScannerTestCase9(unittest.TestCase): path = s.path(env) n = env.File('fff3.f') - def my_rexists(s=n): - s.rexists_called = 1 - return s.old_rexists() - setattr(n, 'old_rexists', n.rexists) - setattr(n, 'rexists', my_rexists) + def my_rexists(s): + s.Tag('rexists_called', 1) + return SCons.Node._rexists_map[s.GetTag('old_rexists')](s) + n.Tag('old_rexists', n._func_rexists) + SCons.Node._rexists_map[3] = my_rexists + n._func_rexists = 3 deps = s(n, env, path) # Make sure rexists() got called on the file node being # scanned, essential for cooperation with VariantDir functionality. - assert n.rexists_called + assert n.GetTag('rexists_called') headers = ['d1/f3.f', 'f3.f'] deps_match(self, deps, headers) diff --git a/src/engine/SCons/Scanner/IDLTests.py b/src/engine/SCons/Scanner/IDLTests.py index 675c70c..227799e 100644 --- a/src/engine/SCons/Scanner/IDLTests.py +++ b/src/engine/SCons/Scanner/IDLTests.py @@ -290,17 +290,18 @@ class IDLScannerTestCase5(unittest.TestCase): path = s.path(env) n = env.File('t3.idl') - def my_rexists(s=n): - s.rexists_called = 1 - return s.old_rexists() - setattr(n, 'old_rexists', n.rexists) - setattr(n, 'rexists', my_rexists) + def my_rexists(s): + s.Tag('rexists_called', 1) + return SCons.Node._rexists_map[s.GetTag('old_rexists')](s) + n.Tag('old_rexists', n._func_rexists) + SCons.Node._rexists_map[3] = my_rexists + n._func_rexists = 3 deps = s(n, env, path) # Make sure rexists() got called on the file node being # scanned, essential for cooperation with VariantDir functionality. - assert n.rexists_called + assert n.GetTag('rexists_called') headers = ['d1/f1.idl', 'd1/f2.idl', 'f1.idl', 'f2.idl', 'f3-test.idl', diff --git a/src/engine/SCons/Script/Main.py b/src/engine/SCons/Script/Main.py index c7a9d27..bd227bd 100644 --- a/src/engine/SCons/Script/Main.py +++ b/src/engine/SCons/Script/Main.py @@ -214,7 +214,7 @@ class BuildTask(SCons.Taskmaster.OutOfDateTask): if self.top and not t.has_builder() and not t.side_effect: if not t.exists(): if t.__class__.__name__ in ('File', 'Dir', 'Entry'): - errstr="Do not know how to make %s target `%s' (%s)." % (t.__class__.__name__, t, t.abspath) + errstr="Do not know how to make %s target `%s' (%s)." % (t.__class__.__name__, t, t.get_abspath()) else: # Alias or Python or ... errstr="Do not know how to make %s target `%s'." % (t.__class__.__name__, t) sys.stderr.write("scons: *** " + errstr) @@ -351,7 +351,7 @@ class CleanTask(SCons.Taskmaster.AlwaysTask): if target in SCons.Environment.CleanTargets: files = SCons.Environment.CleanTargets[target] for f in files: - self.fs_delete(f.abspath, str(f), remove) + self.fs_delete(f.get_abspath(), str(f), remove) def show(self): for t in self._get_files_to_clean(): @@ -484,6 +484,9 @@ def GetOption(name): def SetOption(name, value): return OptionsParser.values.set_option(name, value) +def PrintHelp(file=None): + OptionsParser.print_help(file=file) + # class Stats(object): def __init__(self): @@ -672,7 +675,7 @@ def _set_debug_values(options): if "prepare" in debug_values: SCons.Taskmaster.print_prepare = 1 if "duplicate" in debug_values: - SCons.Node.FS.print_duplicate = 1 + SCons.Node.print_duplicate = 1 def _create_path(plist): path = '.' @@ -946,9 +949,9 @@ def _main(parser): progress_display.set_mode(0) if options.site_dir: - _load_site_scons_dir(d.path, options.site_dir) + _load_site_scons_dir(d.get_internal_path(), options.site_dir) elif not options.no_site_dir: - _load_all_site_scons_dirs(d.path) + _load_all_site_scons_dirs(d.get_internal_path()) if options.include_dir: sys.path = options.include_dir + sys.path @@ -1111,7 +1114,6 @@ def _build_targets(fs, options, targets, target_top): display.set_mode(not options.silent) SCons.Action.print_actions = not options.silent SCons.Action.execute_actions = not options.no_exec - SCons.Node.FS.do_store_info = not options.no_exec SCons.Node.do_store_info = not options.no_exec SCons.SConf.dryrun = options.no_exec diff --git a/src/engine/SCons/Script/SConsOptions.py b/src/engine/SCons/Script/SConsOptions.py index 8ecc093..68c60cc 100644 --- a/src/engine/SCons/Script/SConsOptions.py +++ b/src/engine/SCons/Script/SConsOptions.py @@ -268,7 +268,7 @@ class SConsOptionParser(optparse.OptionParser): preserve_unknown_options = False def error(self, msg): - # overriden OptionValueError exception handler + # overridden OptionValueError exception handler self.print_usage(sys.stderr) sys.stderr.write("SCons Error: %s\n" % msg) sys.exit(2) diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py index f4a7f07..b832ff7 100644 --- a/src/engine/SCons/Script/SConscript.py +++ b/src/engine/SCons/Script/SConscript.py @@ -265,7 +265,7 @@ def _SConscript(fs, *files, **kw): call_stack[-1].globals.update({__file__:old_file}) else: SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning, - "Ignoring missing SConscript '%s'" % f.path) + "Ignoring missing SConscript '%s'" % f.get_internal_path()) finally: SCons.Script.sconscript_reading = SCons.Script.sconscript_reading - 1 @@ -438,7 +438,7 @@ class SConsEnvironment(SCons.Environment.Base): fname = fn.get_path(src_dir) files = [os.path.join(str(variant_dir), fname)] else: - files = [fn.abspath] + files = [fn.get_abspath()] kw['src_dir'] = variant_dir self.fs.VariantDir(variant_dir, src_dir, duplicate) @@ -499,9 +499,9 @@ class SConsEnvironment(SCons.Environment.Base): name = self.subst(name) return SCons.Script.Main.GetOption(name) - def Help(self, text): + def Help(self, text, append=False): text = self.subst(text, raw=1) - SCons.Script.HelpFunction(text) + SCons.Script.HelpFunction(text, append=append) def Import(self, *vars): try: diff --git a/src/engine/SCons/Script/SConscript.xml b/src/engine/SCons/Script/SConscript.xml index c74ad5e..8553fbe 100644 --- a/src/engine/SCons/Script/SConscript.xml +++ b/src/engine/SCons/Script/SConscript.xml @@ -240,7 +240,7 @@ file is found. <scons_function name="Help"> <arguments> -(text) +(text, append=False) </arguments> <summary> <para> @@ -248,12 +248,18 @@ This specifies help text to be printed if the <option>-h</option> argument is given to &scons;. -If +If &f-Help; -is called multiple times, the text is appended together in the order -that +is called multiple times, the text is appended together in the order that &f-Help; -is called. +is called. With append set to False, any +&f-Help; +text generated with +&f-AddOption; +is clobbered. If append is True, the AddOption help is prepended to the help +string, thus preserving the +<option>-h</option> +message. </para> </summary> </scons_function> diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py index bb7b632..6bfc36f 100644 --- a/src/engine/SCons/Script/__init__.py +++ b/src/engine/SCons/Script/__init__.py @@ -41,6 +41,7 @@ start_time = time.time() import collections import os +import StringIO import sys # Special chicken-and-egg handling of the "--debug=memoizer" flag: @@ -107,6 +108,7 @@ QuestionTask = Main.QuestionTask #SConscriptSettableOptions = Main.SConscriptSettableOptions AddOption = Main.AddOption +PrintHelp = Main.PrintHelp GetOption = Main.GetOption SetOption = Main.SetOption Progress = Main.Progress @@ -258,12 +260,25 @@ def _Set_Default_Targets(env, tlist): # help_text = None -def HelpFunction(text): +def HelpFunction(text, append=False): global help_text - if SCons.Script.help_text is None: - SCons.Script.help_text = text - else: - help_text = help_text + text + if help_text is None: + if append: + s = StringIO.StringIO() + PrintHelp(s) + help_text = s.getvalue() + s.close() + else: + help_text = "" +# +# Was in original patch but this text is arbitrary and breaks tests +# so I removed it (Deegan) +# help_text = help_text + "\nLocal Build Variables:\n" + text +# else: +# help_text = help_text + text + + help_text= help_text + text + # # Will be non-zero if we are reading an SConscript file. @@ -318,6 +333,7 @@ GlobalDefaultEnvironmentFunctions = [ 'Ignore', 'Install', 'InstallAs', + 'InstallVersionedLib', 'Literal', 'Local', 'ParseDepends', diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py index 5de1cda..345534e 100644 --- a/src/engine/SCons/Taskmaster.py +++ b/src/engine/SCons/Taskmaster.py @@ -242,7 +242,7 @@ class Task(object): # for t in cached_targets: try: - t.fs.unlink(t.path) + t.fs.unlink(t.get_internal_path()) except (IOError, OSError): pass self.targets[0].build() diff --git a/src/engine/SCons/Tool/MSCommon/arch.py b/src/engine/SCons/Tool/MSCommon/arch.py index 2c90950..9bcc221 100644 --- a/src/engine/SCons/Tool/MSCommon/arch.py +++ b/src/engine/SCons/Tool/MSCommon/arch.py @@ -51,6 +51,12 @@ SupportedArchitectureList = [ 'ia64', ['IA64'], ), + + ArchitectureDefinition( + 'arm', + ['ARM'], + ), + ] SupportedArchitectureMap = {} diff --git a/src/engine/SCons/Tool/MSCommon/vc.py b/src/engine/SCons/Tool/MSCommon/vc.py index 97cb349..0ee6324 100644 --- a/src/engine/SCons/Tool/MSCommon/vc.py +++ b/src/engine/SCons/Tool/MSCommon/vc.py @@ -134,9 +134,13 @@ def get_host_target(env): # If you update this, update SupportedVSList in Tool/MSCommon/vs.py, and the # MSVC_VERSION documentation in Tool/msvc.xml. -_VCVER = ["12.0", "12.0Exp", "11.0", "11.0Exp", "10.0", "10.0Exp", "9.0", "9.0Exp","8.0", "8.0Exp","7.1", "7.0", "6.0"] +_VCVER = ["14.0", "14.0Exp", "12.0", "12.0Exp", "11.0", "11.0Exp", "10.0", "10.0Exp", "9.0", "9.0Exp","8.0", "8.0Exp","7.1", "7.0", "6.0"] _VCVER_TO_PRODUCT_DIR = { + '14.0' : [ + r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir'], + '14.0Exp' : [ + r'Microsoft\VCExpress\14.0\Setup\VC\ProductDir'], '12.0' : [ r'Microsoft\VisualStudio\12.0\Setup\VC\ProductDir'], '12.0Exp' : [ diff --git a/src/engine/SCons/Tool/MSCommon/vs.py b/src/engine/SCons/Tool/MSCommon/vs.py index 2ec403b..d9eb1e3 100644 --- a/src/engine/SCons/Tool/MSCommon/vs.py +++ b/src/engine/SCons/Tool/MSCommon/vs.py @@ -203,6 +203,28 @@ class VisualStudio(object): # Tool/MSCommon/vc.py, and the MSVC_VERSION documentation in Tool/msvc.xml. SupportedVSList = [ + # Visual Studio 2015 + VisualStudio('14.0', + vc_version='14.0', + sdk_version='10.0A', + hkeys=[r'Microsoft\VisualStudio\14.0\Setup\VS\ProductDir'], + common_tools_var='VS140COMNTOOLS', + executable_path=r'Common7\IDE\devenv.com', + batch_file_path=r'Common7\Tools\vsvars32.bat', + supported_arch=['x86', 'amd64', "arm"], + ), + + # Visual C++ 2015 Express Edition (for Desktop) + VisualStudio('14.0Exp', + vc_version='14.0', + sdk_version='10.0A', + hkeys=[r'Microsoft\VisualStudio\14.0\Setup\VS\ProductDir'], + common_tools_var='VS140COMNTOOLS', + executable_path=r'Common7\IDE\WDExpress.exe', + batch_file_path=r'Common7\Tools\vsvars32.bat', + supported_arch=['x86', 'amd64', "arm"], + ), + # Visual Studio 2013 VisualStudio('12.0', vc_version='12.0', diff --git a/src/engine/SCons/Tool/__init__.py b/src/engine/SCons/Tool/__init__.py index 2621cf9..7374687 100644 --- a/src/engine/SCons/Tool/__init__.py +++ b/src/engine/SCons/Tool/__init__.py @@ -236,150 +236,436 @@ def createStaticLibBuilder(env): return static_lib -def VersionShLibLinkNames(version, libname, env): - """Generate names of symlinks to the versioned shared library""" +def _call_linker_cb(env, callback, args, result = None): + """Returns the result of env['LINKCALLBACKS'][callback](*args) + if env['LINKCALLBACKS'] is a dictionary and env['LINKCALLBACKS'][callback] + is callable. If these conditions are not met, return the value provided as + the *result* argument. This function is mainly used for generating library + info such as versioned suffixes, symlink maps, sonames etc. by delegating + the core job to callbacks configured by current linker tool""" + Verbose = False - platform = env.subst('$PLATFORM') - shlib_suffix = env.subst('$SHLIBSUFFIX') - shlink_flags = SCons.Util.CLVar(env.subst('$SHLINKFLAGS')) - - linknames = [] - if version.count(".") != 2: - # We need a version string of the form x.y.z to proceed - # Several changes need to be made to support versions like x.y - raise ValueError - - if platform == 'darwin': - # For libfoo.x.y.z.dylib, linknames libfoo.so - suffix_re = re.escape('.' + version + shlib_suffix) - linkname = re.sub(suffix_re, shlib_suffix, libname) + + if Verbose: + print '_call_linker_cb: args=%r' % args + print '_call_linker_cb: callback=%r' % callback + + try: + cbfun = env['LINKCALLBACKS'][callback] + except (KeyError, TypeError): if Verbose: - print "VersionShLibLinkNames: linkname = ",linkname - linknames.append(linkname) - elif platform == 'posix' or platform == 'sunos': - if sys.platform.startswith('openbsd'): - # OpenBSD uses x.y shared library versioning numbering convention - # and doesn't use symlinks to backwards-compatible libraries - return [] - # For libfoo.so.x.y.z, linknames libfoo.so libfoo.so.x.y libfoo.so.x - suffix_re = re.escape(shlib_suffix + '.' + version) - # First linkname has no version number - linkname = re.sub(suffix_re, shlib_suffix, libname) + print '_call_linker_cb: env["LINKCALLBACKS"][%r] not found or can not be used' % callback + pass + else: if Verbose: - print "VersionShLibLinkNames: linkname = ",linkname - linknames.append(linkname) - versionparts = version.split('.') - major_name = linkname + "." + versionparts[0] - minor_name = major_name + "." + versionparts[1] - #Only add link for major_name - #for linkname in [major_name, minor_name]: - for linkname in [major_name, ]: + print '_call_linker_cb: env["LINKCALLBACKS"][%r] found' % callback + print '_call_linker_cb: env["LINKCALLBACKS"][%r]=%r' % (callback, cbfun) + if(callable(cbfun)): if Verbose: - print "VersionShLibLinkNames: linkname ",linkname, ", target ",libname - linknames.append(linkname) - # note: no Windows case here (win32 or cygwin); - # MSVC doesn't support this type of versioned shared libs. - # (could probably do something for MinGW though) - return linknames - -def VersionedSharedLibrary(target = None, source= None, env=None): - """Build a shared library. If the environment has SHLIBVERSION -defined make a versioned shared library and create the appropriate -symlinks for the platform we are on""" - Verbose = False - try: - version = env.subst('$SHLIBVERSION') - except KeyError: - version = None + print '_call_linker_cb: env["LINKCALLBACKS"][%r] is callable' % callback + result = cbfun(env, *args) + return result - # libname includes the version number if one was given - libname = getattr(target[0].attributes, 'shlibname', target[0].name) - platform = env.subst('$PLATFORM') - shlib_suffix = env.subst('$SHLIBSUFFIX') - shlink_flags = SCons.Util.CLVar(env.subst('$SHLINKFLAGS')) - if Verbose: - print "VersionShLib: libname = ",libname - print "VersionShLib: platform = ",platform - print "VersionShLib: shlib_suffix = ",shlib_suffix - print "VersionShLib: target = ",str(target[0]) - - if version: - # set the shared library link flags - if platform == 'posix': - shlink_flags += [ '-Wl,-Bsymbolic' ] - # OpenBSD doesn't usually use SONAME for libraries - if not sys.platform.startswith('openbsd'): - # continue setup of shlink flags for all other POSIX systems - suffix_re = re.escape(shlib_suffix + '.' + version) - (major, age, revision) = version.split(".") - # soname will have only the major version number in it - soname = re.sub(suffix_re, shlib_suffix, libname) + '.' + major - shlink_flags += [ '-Wl,-soname=%s' % soname ] - if Verbose: - print " soname ",soname,", shlink_flags ",shlink_flags - elif platform == 'sunos': - suffix_re = re.escape(shlib_suffix + '.' + version) - (major, age, revision) = version.split(".") - soname = re.sub(suffix_re, shlib_suffix, libname) + '.' + major - shlink_flags += [ '-h', soname ] - elif platform == 'cygwin': - shlink_flags += [ '-Wl,-Bsymbolic', - '-Wl,--out-implib,${TARGET.base}.a' ] - elif platform == 'darwin': - shlink_flags += [ '-current_version', '%s' % version, - '-compatibility_version', '%s' % version, - '-undefined', 'dynamic_lookup' ] +def _call_env_subst(env, string, *args, **kw): + kw2 = {} + for k in ('raw', 'target', 'source', 'conv', 'executor'): + try: kw2[k] = kw[k] + except KeyError: pass + return env.subst(string, *args, **kw2) + +class _ShLibInfoSupport(object): + def get_libtype(self): + return 'ShLib' + def get_lib_prefix(self, env, *args, **kw): + return _call_env_subst(env,'$SHLIBPREFIX', *args, **kw) + def get_lib_suffix(self, env, *args, **kw): + return _call_env_subst(env,'$SHLIBSUFFIX', *args, **kw) + def get_lib_version(self, env, *args, **kw): + return _call_env_subst(env,'$SHLIBVERSION', *args, **kw) + def get_lib_noversionsymlinks(self, env, *args, **kw): + return _call_env_subst(env,'$SHLIBNOVERSIONSYMLINKS', *args, **kw) + +class _LdModInfoSupport(object): + def get_libtype(self): + return 'LdMod' + def get_lib_prefix(self, env, *args, **kw): + return _call_env_subst(env,'$LDMODULEPREFIX', *args, **kw) + def get_lib_suffix(self, env, *args, **kw): + return _call_env_subst(env,'$LDMODULESUFFIX', *args, **kw) + def get_lib_version(self, env, *args, **kw): + return _call_env_subst(env,'$LDMODULEVERSION', *args, **kw) + def get_lib_noversionsymlinks(self, env, *args, **kw): + return _call_env_subst(env,'$LDMODULENOVERSIONSYMLINKS', *args, **kw) + +class _ImpLibInfoSupport(object): + def get_libtype(self): + return 'ImpLib' + def get_lib_prefix(self, env, *args, **kw): + return _call_env_subst(env,'$IMPLIBPREFIX', *args, **kw) + def get_lib_suffix(self, env, *args, **kw): + return _call_env_subst(env,'$IMPLIBSUFFIX', *args, **kw) + def get_lib_version(self, env, *args, **kw): + version = _call_env_subst(env,'$IMPLIBVERSION', *args, **kw) + if not version: + try: lt = kw['implib_libtype'] + except KeyError: pass + else: + if lt == 'ShLib': + version = _call_env_subst(env,'$SHLIBVERSION', *args, **kw) + elif lt == 'LdMod': + version = _call_env_subst(env,'$LDMODULEVERSION', *args, **kw) + return version + def get_lib_noversionsymlinks(self, env, *args, **kw): + disable = None + try: env['IMPLIBNOVERSIONSYMLINKS'] + except KeyError: + try: lt = kw['implib_libtype'] + except KeyError: pass + else: + if lt == 'ShLib': + disable = _call_env_subst(env,'$SHLIBNOVERSIONSYMLINKS', *args, **kw) + elif lt == 'LdMod': + disable = _call_env_subst(env,'$LDMODULENOVERSIONSYMLINKS', *args, **kw) + else: + disable = _call_env_subst(env,'$IMPLIBNOVERSIONSYMLINKS', *args, **kw) + return disable + +class _LibInfoGeneratorBase(object): + """Generator base class for library-related info such as suffixes for + versioned libraries, symlink maps, sonames etc. It handles commonities + of SharedLibrary and LoadableModule + """ + _support_classes = { 'ShLib' : _ShLibInfoSupport, + 'LdMod' : _LdModInfoSupport, + 'ImpLib' : _ImpLibInfoSupport } + def __init__(self, libtype, infoname): + self.set_libtype(libtype) + self.set_infoname(infoname) + + def set_libtype(self, libtype): + try: + support_class = self._support_classes[libtype] + except KeyError: + raise ValueError('unsupported libtype %r' % libtype) + self._support = support_class() + + def get_libtype(self): + return self._support.get_libtype() + + def set_infoname(self, infoname): + self.infoname = infoname + + def get_infoname(self): + return self.infoname + + def get_lib_prefix(self, env, *args, **kw): + return self._support.get_lib_prefix(env,*args,**kw) + + def get_lib_suffix(self, env, *args, **kw): + return self._support.get_lib_suffix(env,*args,**kw) + + def get_lib_version(self, env, *args, **kw): + return self._support.get_lib_version(env,*args,**kw) + + def get_lib_noversionsymlinks(self, env, *args, **kw): + return self._support.get_lib_noversionsymlinks(env,*args,**kw) + + # Returns name of generator linker callback that shall be used to generate + # our info for a versioned library. For example, if our libtype is 'ShLib' + # and infoname is 'Prefix', it would return 'VersionedShLibPrefix'. + def get_versioned_lib_info_generator(self, **kw): + try: libtype = kw['generator_libtype'] + except KeyError: libtype = self.get_libtype() + infoname = self.get_infoname() + return 'Versioned%s%s' % (libtype, infoname) + + def generate_versioned_lib_info(self, env, args, result = None, **kw): + callback = self.get_versioned_lib_info_generator(**kw) + return _call_linker_cb(env, callback, args, result) + +class _LibPrefixGenerator(_LibInfoGeneratorBase): + """Library prefix generator, used as target_prefix in SharedLibrary and + LoadableModule builders""" + def __init__(self, libtype): + super(_LibPrefixGenerator, self).__init__(libtype, 'Prefix') + + def __call__(self, env, sources = None, **kw): + Verbose = False + + if sources and 'source' not in kw: + kw2 = kw.copy() + kw2['source'] = sources + else: + kw2 = kw + + prefix = self.get_lib_prefix(env,**kw2) if Verbose: - print "VersionShLib: shlink_flags = ",shlink_flags - envlink = env.Clone() - envlink['SHLINKFLAGS'] = shlink_flags - else: - envlink = env + print "_LibPrefixGenerator: input prefix=%r" % prefix + + version = self.get_lib_version(env, **kw2) + if Verbose: + print "_LibPrefixGenerator: version=%r" % version - result = SCons.Defaults.ShLinkAction(target, source, envlink) + if version: + prefix = self.generate_versioned_lib_info(env, [prefix, version], prefix, **kw2) - if version: - # here we need the full pathname so the links end up in the right directory - libname = getattr(target[0].attributes, 'shlibpath', target[0].path) if Verbose: - print "VerShLib: target lib is = ", libname - print "VerShLib: name is = ", target[0].name - print "VerShLib: dir is = ", target[0].dir.path - linknames = VersionShLibLinkNames(version, libname, env) + print "_LibPrefixGenerator: return prefix=%r" % prefix + return prefix + +ShLibPrefixGenerator = _LibPrefixGenerator('ShLib') +LdModPrefixGenerator = _LibPrefixGenerator('LdMod') +ImpLibPrefixGenerator = _LibPrefixGenerator('ImpLib') + +class _LibSuffixGenerator(_LibInfoGeneratorBase): + """Library suffix generator, used as target_suffix in SharedLibrary and + LoadableModule builders""" + def __init__(self, libtype): + super(_LibSuffixGenerator, self).__init__(libtype, 'Suffix') + + def __call__(self, env, sources = None, **kw): + Verbose = False + + if sources and 'source' not in kw: + kw2 = kw.copy() + kw2['source'] = sources + else: + kw2 = kw + + suffix = self.get_lib_suffix(env, **kw2) if Verbose: - print "VerShLib: linknames ",linknames - # Here we just need the file name w/o path as the target of the link - lib_ver = getattr(target[0].attributes, 'shlibname', target[0].name) - # make symlink of adjacent names in linknames - for count in range(len(linknames)): - linkname = linknames[count] - if count > 0: - try: - os.remove(lastlinkname) - except: - pass - os.symlink(os.path.basename(linkname),lastlinkname) - if Verbose: - print "VerShLib: made sym link of %s -> %s" % (lastlinkname,linkname) - lastlinkname = linkname - # finish chain of sym links with link to the actual library - if len(linknames)>0: + print "_LibSuffixGenerator: input suffix=%r" % suffix + + version = self.get_lib_version(env, **kw2) + if Verbose: + print "_LibSuffixGenerator: version=%r" % version + + if version: + suffix = self.generate_versioned_lib_info(env, [suffix, version], suffix, **kw2) + + if Verbose: + print "_LibSuffixGenerator: return suffix=%r" % suffix + return suffix + +ShLibSuffixGenerator = _LibSuffixGenerator('ShLib') +LdModSuffixGenerator = _LibSuffixGenerator('LdMod') +ImpLibSuffixGenerator = _LibSuffixGenerator('ImpLib') + +class _LibSymlinkGenerator(_LibInfoGeneratorBase): + """Library symlink map generator. It generates a list of symlinks that + should be created by SharedLibrary or LoadableModule builders""" + def __init__(self, libtype): + super(_LibSymlinkGenerator, self).__init__(libtype, 'Symlinks') + + def __call__(self, env, libnode, **kw): + Verbose = False + + if libnode and 'target' not in kw: + kw2 = kw.copy() + kw2['target'] = libnode + else: + kw2 = kw + + if Verbose: + print "_LibSymLinkGenerator: libnode=%r" % libnode.get_path() + + symlinks = None + + version = self.get_lib_version(env, **kw2) + disable = self.get_lib_noversionsymlinks(env, **kw2) + if Verbose: + print '_LibSymlinkGenerator: version=%r' % version + print '_LibSymlinkGenerator: disable=%r' % disable + + if version and not disable: + prefix = self.get_lib_prefix(env,**kw2) + suffix = self.get_lib_suffix(env,**kw2) + symlinks = self.generate_versioned_lib_info(env, [libnode, version, prefix, suffix], **kw2) + + if Verbose: + print '_LibSymlinkGenerator: return symlinks=%r' % StringizeLibSymlinks(symlinks) + return symlinks + +ShLibSymlinkGenerator = _LibSymlinkGenerator('ShLib') +LdModSymlinkGenerator = _LibSymlinkGenerator('LdMod') +ImpLibSymlinkGenerator = _LibSymlinkGenerator('ImpLib') + +class _LibNameGenerator(_LibInfoGeneratorBase): + """Generates "unmangled" library name from a library file node. + + Generally, it's thought to revert modifications done by prefix/suffix + generators (_LibPrefixGenerator/_LibSuffixGenerator) used by a library + builder. For example, on gnulink the suffix generator used by SharedLibrary + builder appends $SHLIBVERSION to $SHLIBSUFFIX producing node name which + ends with "$SHLIBSUFFIX.$SHLIBVERSION". Correspondingly, the implementation + of _LibNameGenerator replaces "$SHLIBSUFFIX.$SHLIBVERSION" with + "$SHLIBSUFFIX" in the node's basename. So that, if $SHLIBSUFFIX is ".so", + $SHLIBVERSION is "0.1.2" and the node path is "/foo/bar/libfoo.so.0.1.2", + the _LibNameGenerator shall return "libfoo.so". Other link tools may + implement it's own way of library name unmangling. + """ + def __init__(self, libtype): + super(_LibNameGenerator, self).__init__(libtype, 'Name') + + def __call__(self, env, libnode, **kw): + """Returns "demangled" library name""" + Verbose = False + + if libnode and 'target' not in kw: + kw2 = kw.copy() + kw2['target'] = libnode + else: + kw2 = kw + + if Verbose: + print "_LibNameGenerator: libnode=%r" % libnode.get_path() + + version = self.get_lib_version(env, **kw2) + if Verbose: + print '_LibNameGenerator: version=%r' % version + + name = None + if version: + prefix = self.get_lib_prefix(env,**kw2) + suffix = self.get_lib_suffix(env,**kw2) + name = self.generate_versioned_lib_info(env, [libnode, version, prefix, suffix], **kw2) + + if not name: + name = os.path.basename(libnode.get_path()) + + if Verbose: + print '_LibNameGenerator: return name=%r' % name + + return name + +ShLibNameGenerator = _LibNameGenerator('ShLib') +LdModNameGenerator = _LibNameGenerator('LdMod') +ImpLibNameGenerator = _LibNameGenerator('ImpLib') + +class _LibSonameGenerator(_LibInfoGeneratorBase): + """Library soname generator. Returns library soname (e.g. libfoo.so.0) for + a given node (e.g. /foo/bar/libfoo.so.0.1.2)""" + def __init__(self, libtype): + super(_LibSonameGenerator, self).__init__(libtype, 'Soname') + + def __call__(self, env, libnode, **kw): + """Returns a SONAME based on a shared library's node path""" + Verbose = False + + if libnode and 'target' not in kw: + kw2 = kw.copy() + kw2['target'] = libnode + else: + kw2 = kw + + if Verbose: + print "_LibSonameGenerator: libnode=%r" % libnode.get_path() + + soname = _call_env_subst(env, '$SONAME', **kw2) + if not soname: + version = self.get_lib_version(env,**kw2) + if Verbose: + print "_LibSonameGenerator: version=%r" % version + if version: + prefix = self.get_lib_prefix(env,**kw2) + suffix = self.get_lib_suffix(env,**kw2) + soname = self.generate_versioned_lib_info(env, [libnode, version, prefix, suffix], **kw2) + + if not soname: + # fallback to library name (as returned by appropriate _LibNameGenerator) + soname = _LibNameGenerator(self.get_libtype())(env, libnode) + if Verbose: + print "_LibSonameGenerator: FALLBACK: soname=%r" % soname + + if Verbose: + print "_LibSonameGenerator: return soname=%r" % soname + + return soname + +ShLibSonameGenerator = _LibSonameGenerator('ShLib') +LdModSonameGenerator = _LibSonameGenerator('LdMod') + +def StringizeLibSymlinks(symlinks): + """Converts list with pairs of nodes to list with pairs of node paths + (strings). Used mainly for debugging.""" + if SCons.Util.is_List(symlinks): + try: + return [ (k.get_path(), v.get_path()) for k,v in symlinks ] + except (TypeError, ValueError): + return symlinks + else: + return symlinks + +def EmitLibSymlinks(env, symlinks, libnode, **kw): + """Used by emitters to handle (shared/versioned) library symlinks""" + Verbose = False + + # nodes involved in process... all symlinks + library + nodes = list(set([ x for x,y in symlinks ] + [libnode])) + + clean_targets = kw.get('clean_targets', []) + if not SCons.Util.is_List(clean_targets): + clean_targets = [ clean_targets ] + + for link, linktgt in symlinks: + env.SideEffect(link, linktgt) + if(Verbose): + print "EmitLibSymlinks: SideEffect(%r,%r)" % (link.get_path(), linktgt.get_path()) + clean_list = filter(lambda x : x != linktgt, nodes) + env.Clean(list(set([linktgt] + clean_targets)), clean_list) + if(Verbose): + print "EmitLibSymlinks: Clean(%r,%r)" % (linktgt.get_path(), map(lambda x : x.get_path(), clean_list)) + +def CreateLibSymlinks(env, symlinks): + """Physically creates symlinks. The symlinks argument must be a list in + form [ (link, linktarget), ... ], where link and linktarget are SCons + nodes. + """ + + Verbose = False + for link, linktgt in symlinks: + linktgt = link.get_dir().rel_path(linktgt) + link = link.get_path() + if(Verbose): + print "CreateLibSymlinks: preparing to add symlink %r -> %r" % (link, linktgt) + # Delete the (previously created) symlink if exists. Let only symlinks + # to be deleted to prevent accidental deletion of source files... + if env.fs.islink(link): + env.fs.unlink(link) + if(Verbose): + print "CreateLibSymlinks: removed old symlink %r" % link + # If a file or directory exists with the same name as link, an OSError + # will be thrown, which should be enough, I think. + env.fs.symlink(linktgt, link) + if(Verbose): + print "CreateLibSymlinks: add symlink %r -> %r" % (link, linktgt) + return 0 + +def LibSymlinksActionFunction(target, source, env): + for tgt in target: + symlinks = getattr(getattr(tgt,'attributes', None), 'shliblinks', None) + if symlinks: + CreateLibSymlinks(env, symlinks) + return 0 + +def LibSymlinksStrFun(target, source, env, *args): + cmd = None + for tgt in target: + symlinks = getattr(getattr(tgt,'attributes', None), 'shliblinks', None) + if symlinks: + if cmd is None: cmd = "" + if cmd: cmd += "\n" + cmd += "Create symlinks for: %r" % tgt.get_path() try: - os.remove(lastlinkname) - except: + linkstr = ', '.join([ "%r->%r" %(k,v) for k,v in StringizeLibSymlinks(symlinks)]) + except (KeyError, ValueError): pass - os.symlink(lib_ver,lastlinkname) - if Verbose: - print "VerShLib: made sym link of %s -> %s" % (linkname, lib_ver) - return result + else: + cmd += ": %s" % linkstr + return cmd + -# Fix http://scons.tigris.org/issues/show_bug.cgi?id=2903 : -# Ensure we still depend on SCons.Defaults.ShLinkAction command line which is $SHLINKCOM. -# This was tricky because we don't want changing LIBPATH to cause a rebuild, but -# changing other link args should. LIBPATH has $( ... $) around it but until this -# fix, when the varlist was added to the build sig those ignored parts weren't getting -# ignored. -ShLibAction = SCons.Action.Action(VersionedSharedLibrary, None, varlist=['SHLINKCOM']) +LibSymlinksAction = SCons.Action.Action(LibSymlinksActionFunction, LibSymlinksStrFun) def createSharedLibBuilder(env): """This is a utility function that creates the SharedLibrary @@ -393,11 +679,12 @@ def createSharedLibBuilder(env): except KeyError: import SCons.Defaults action_list = [ SCons.Defaults.SharedCheck, - ShLibAction ] + SCons.Defaults.ShLinkAction, + LibSymlinksAction ] shared_lib = SCons.Builder.Builder(action = action_list, emitter = "$SHLIBEMITTER", - prefix = '$SHLIBPREFIX', - suffix = '$SHLIBSUFFIX', + prefix = ShLibPrefixGenerator, + suffix = ShLibSuffixGenerator, target_scanner = ProgramScanner, src_suffix = '$SHOBJSUFFIX', src_builder = 'SharedObject') @@ -417,11 +704,12 @@ def createLoadableModuleBuilder(env): except KeyError: import SCons.Defaults action_list = [ SCons.Defaults.SharedCheck, - SCons.Defaults.LdModuleLinkAction ] + SCons.Defaults.LdModuleLinkAction, + LibSymlinksAction ] ld_module = SCons.Builder.Builder(action = action_list, emitter = "$LDMODULEEMITTER", - prefix = '$LDMODULEPREFIX', - suffix = '$LDMODULESUFFIX', + prefix = LdModPrefixGenerator, + suffix = LdModSuffixGenerator, target_scanner = ProgramScanner, src_suffix = '$SHOBJSUFFIX', src_builder = 'SharedObject') diff --git a/src/engine/SCons/Tool/__init__.xml b/src/engine/SCons/Tool/__init__.xml index ee56fc3..69cc597 100644 --- a/src/engine/SCons/Tool/__init__.xml +++ b/src/engine/SCons/Tool/__init__.xml @@ -200,22 +200,20 @@ For maximum portability, use the &b-LoadableModule; builder for the latter. When the &cv-link-SHLIBVERSION; construction variable is defined a versioned shared library is created. This modifies the &cv-link-SHLINKFLAGS; as required, adds the version number to the library name, and creates the symlinks that -are needed. &cv-link-SHLIBVERSION; needs to be of the form X.Y.Z, where X -and Y are numbers, and Z is a number but can also contain letters to designate -alpha, beta, or release candidate patch levels. +are needed. </para> <example_commands> env.SharedLibrary(target = 'bar', source = ['bar.c', 'foo.o'], SHLIBVERSION='1.5.2') </example_commands> - <para> -This builder may create multiple links to the library. On a POSIX system, -for the shared library libbar.so.2.3.1, the links created would be -libbar.so and libbar.so.2; on a Darwin (OSX) system -the library would be libbar.2.3.1.dylib and the link would be -libbar.dylib. +On a POSIX system, versions with a single token create exactly one symlink: +libbar.so.6 would have symlinks libbar.so only. +On a POSIX system, versions with two or more +tokens create exactly two symlinks: libbar.so.2.3.1 would have symlinks +libbar.so and libbar.so.2; on a Darwin (OSX) system the library would be +libbar.2.3.1.dylib and the link would be libbar.dylib. </para> <para> @@ -464,6 +462,17 @@ as C++ files. </summary> </cvar> +<cvar name="IMPLIBVERSION"> +<summary> +<para> +Used to override &cv-link-SHLIBVERSION;/&cv-link-LDMODULEVERSION; when +generating versioned import library for a shared library/loadable module. If +undefined, the &cv-link-SHLIBVERSION;/&cv-link-LDMODULEVERSION; is used to +determine the version of versioned import library. +</para> +</summary> +</cvar> + <cvar name="LIBEMITTER"> <summary> <para> @@ -472,6 +481,19 @@ TODO </summary> </cvar> +<cvar name="LDMODULEVERSION"> +<summary> +<para> +When this construction variable is defined, a versioned loadable module +is created by &b-link-LoadableModule; builder. This activates the +&cv-link-_LDMODULEVERSIONFLAGS; and thus modifies the &cv-link-LDMODULECOM; as +required, adds the version number to the library name, and creates the symlinks +that are needed. &cv-link-LDMODULEVERSION; versions should exist in the same +format as &cv-link-SHLIBVERSION;. +</para> +</summary> +</cvar> + <cvar name="SHLIBEMITTER"> <summary> <para> @@ -492,11 +514,12 @@ TODO <summary> <para> When this construction variable is defined, a versioned shared library -is created. This modifies the &cv-link-SHLINKFLAGS; as required, adds -the version number to the library name, and creates the symlinks that -are needed. &cv-link-SHLIBVERSION; needs to be of the form X.Y.Z, -where X and Y are numbers, and Z is a number but can also contain -letters to designate alpha, beta, or release candidate patch levels. +is created by &b-link-SharedLibrary; builder. This activates the +&cv-link-_SHLIBVERSIONFLAGS; and thus modifies the &cv-link-SHLINKCOM; as +required, adds the version number to the library name, and creates the symlinks +that are needed. &cv-link-SHLIBVERSION; versions should exist as alpha-numeric, +decimal-delimited values as defined by the regular expression "\w+[\.\w+]*". +Example &cv-link-SHLIBVERSION; values include '1', '1.2.3', and '1.2.gitaa412c8b'. </para> </summary> </cvar> diff --git a/src/engine/SCons/Tool/cyglink.py b/src/engine/SCons/Tool/cyglink.py index 87716cf..5230910 100644 --- a/src/engine/SCons/Tool/cyglink.py +++ b/src/engine/SCons/Tool/cyglink.py @@ -7,19 +7,27 @@ It will usually be imported through the generic SCons.Tool.Tool() selection method. """ +import re +import os import SCons.Action import SCons.Util +import SCons.Tool import gnulink +import link -def shlib_generator(target, source, env, for_signature): - cmd = SCons.Util.CLVar(['$SHLINK']) +def _lib_generator(target, source, env, for_signature, **kw): + try: cmd = kw['cmd'] + except KeyError: cmd = SCons.Util.CLVar(['$SHLINK']) + + try: vp = kw['varprefix'] + except KeyError: vp = 'SHLIB' - dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX') + dll = env.FindIxes(target, '%sPREFIX' % vp, '%sSUFFIX' % vp) if dll: cmd.extend(['-o', dll]) - cmd.extend(['$SHLINKFLAGS', '$__RPATH']) + cmd.extend(['$SHLINKFLAGS', '$__%sVERSIONFLAGS' % vp, '$__RPATH']) implib = env.FindIxes(target, 'IMPLIBPREFIX', 'IMPLIBSUFFIX') if implib: @@ -35,37 +43,141 @@ def shlib_generator(target, source, env, for_signature): return [cmd] -def shlib_emitter(target, source, env): - dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX') + +def shlib_generator(target, source, env, for_signature): + return _lib_generator(target, source, env, for_signature, + varprefix='SHLIB', + cmd = SCons.Util.CLVar(['$SHLINK'])) + +def ldmod_generator(target, source, env, for_signature): + return _lib_generator(target, source, env, for_signature, + varprefix='LDMODULE', + cmd = SCons.Util.CLVar(['$LDMODULE'])) + +def _lib_emitter(target, source, env, **kw): + Verbose = False + + if Verbose: + print "_lib_emitter: target[0]=%r" % target[0].get_path() + + try: vp = kw['varprefix'] + except KeyError: vp = 'SHLIB' + + try: libtype = kw['libtype'] + except KeyError: libtype = 'ShLib' + + dll = env.FindIxes(target, '%sPREFIX' % vp, '%sSUFFIX' % vp) no_import_lib = env.get('no_import_lib', 0) + if Verbose: + print "_lib_emitter: dll=%r" % dll.get_path() + if not dll or len(target) > 1: - raise SCons.Errors.UserError("A shared library should have exactly one target with the suffix: %s" % env.subst("$SHLIBSUFFIX")) + raise SCons.Errors.UserError("A shared library should have exactly one target with the suffix: %s" % env.subst("$%sSUFFIX" % vp)) # Remove any "lib" after the prefix - pre = env.subst('$SHLIBPREFIX') + pre = env.subst('$%sPREFIX' % vp) if dll.name[len(pre):len(pre)+3] == 'lib': dll.name = pre + dll.name[len(pre)+3:] + if Verbose: + print "_lib_emitter: dll.name=%r" % dll.name + orig_target = target target = [env.fs.File(dll)] target[0].attributes.shared = 1 + if Verbose: + print "_lib_emitter: after target=[env.fs.File(dll)]: target[0]=%r" % target[0].get_path() + # Append an import lib target if not no_import_lib: # Create list of target libraries as strings target_strings = env.ReplaceIxes(orig_target[0], - 'SHLIBPREFIX', 'SHLIBSUFFIX', + '%sPREFIX' % vp, '%sSUFFIX' % vp, 'IMPLIBPREFIX', 'IMPLIBSUFFIX') + if Verbose: + print "_lib_emitter: target_strings=%r" % target_strings implib_target = env.fs.File(target_strings) + if Verbose: + print "_lib_emitter: implib_target=%r" % implib_target.get_path() implib_target.attributes.shared = 1 target.append(implib_target) + symlinks = SCons.Tool.ImpLibSymlinkGenerator(env, implib_target, + implib_libtype=libtype, + generator_libtype=libtype+'ImpLib') + if Verbose: + print "_lib_emitter: implib symlinks=%r" % SCons.Tool.StringizeLibSymlinks(symlinks) + if symlinks: + SCons.Tool.EmitLibSymlinks(env, symlinks, implib_target, clean_targets = target[0]) + implib_target.attributes.shliblinks = symlinks + return (target, source) + +def shlib_emitter(target, source, env): + return _lib_emitter(target, source, env, varprefix='SHLIB', libtype='ShLib') + +def ldmod_emitter(target, source, env): + return _lib_emitter(target, source, env, varprefix='LDMODULE', libtype='LdMod') +def _versioned_lib_suffix(env, suffix, version): + """Generate versioned shared library suffix from a unversioned one. + If suffix='.dll', and version='0.1.2', then it returns '-0-1-2.dll'""" + Verbose = False + if Verbose: + print "_versioned_lib_suffix: suffix= ", suffix + print "_versioned_lib_suffix: version= ", version + cygversion = re.sub('\.', '-', version) + if not suffix.startswith('-' + cygversion): + suffix = '-' + cygversion + suffix + if Verbose: + print "_versioned_lib_suffix: return suffix= ", suffix + return suffix + +def _versioned_implib_name(env, libnode, version, prefix, suffix, **kw): + return link._versioned_lib_name(env, libnode, version, prefix, suffix, + SCons.Tool.ImpLibPrefixGenerator, + SCons.Tool.ImpLibSuffixGenerator, + implib_libtype=kw['libtype']) + +def _versioned_implib_symlinks(env, libnode, version, prefix, suffix, **kw): + """Generate link names that should be created for a versioned shared lirbrary. + Returns a list in the form [ (link, linktarget), ... ] + """ + Verbose = False + + if Verbose: + print "_versioned_implib_symlinks: libnode=%r" % libnode.get_path() + print "_versioned_implib_symlinks: version=%r" % version + + try: libtype = kw['libtype'] + except KeyError: libtype = 'ShLib' + + + linkdir = os.path.dirname(libnode.get_path()) + if Verbose: + print "_versioned_implib_symlinks: linkdir=%r" % linkdir + + name = SCons.Tool.ImpLibNameGenerator(env, libnode, + implib_libtype=libtype, + generator_libtype=libtype+'ImpLib') + if Verbose: + print "_versioned_implib_symlinks: name=%r" % name + + major = version.split('.')[0] + + link0 = env.fs.File(os.path.join(linkdir, name)) + symlinks = [(link0, libnode)] + + if Verbose: + print "_versioned_implib_symlinks: return symlinks=%r" % SCons.Tool.StringizeLibSymlinks(symlinks) + + return symlinks shlib_action = SCons.Action.Action(shlib_generator, generator=1) +ldmod_action = SCons.Action.Action(ldmod_generator, generator=1) def generate(env): """Add Builders and construction variables for cyglink to an Environment.""" @@ -74,8 +186,9 @@ def generate(env): env['LINKFLAGS'] = SCons.Util.CLVar('-Wl,-no-undefined') env['SHLINKCOM'] = shlib_action - env['LDMODULECOM'] = shlib_action + env['LDMODULECOM'] = ldmod_action env.Append(SHLIBEMITTER = [shlib_emitter]) + env.Append(LDMODULEEMITTER = [ldmod_emitter]) env['SHLIBPREFIX'] = 'cyg' env['SHLIBSUFFIX'] = '.dll' @@ -83,6 +196,31 @@ def generate(env): env['IMPLIBPREFIX'] = 'lib' env['IMPLIBSUFFIX'] = '.dll.a' + # Variables used by versioned shared libraries + env['_SHLIBVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS' + env['_LDMODULEVERSIONFLAGS'] = '$LDMODULEVERSIONFLAGS' + + # SHLIBVERSIONFLAGS and LDMODULEVERSIONFLAGS are same as in gnulink... + + # LINKCALLBACKS are NOT inherited from gnulink + env['LINKCALLBACKS'] = { + 'VersionedShLibSuffix' : _versioned_lib_suffix, + 'VersionedLdModSuffix' : _versioned_lib_suffix, + 'VersionedImpLibSuffix' : _versioned_lib_suffix, + 'VersionedShLibName' : link._versioned_shlib_name, + 'VersionedLdModName' : link._versioned_ldmod_name, + 'VersionedShLibImpLibName' : lambda *args: _versioned_implib_name(*args, libtype='ShLib'), + 'VersionedLdModImpLibName' : lambda *args: _versioned_implib_name(*args, libtype='LdMod'), + 'VersionedShLibImpLibSymlinks' : lambda *args: _versioned_implib_symlinks(*args, libtype='ShLib'), + 'VersionedLdModImpLibSymlinks' : lambda *args: _versioned_implib_symlinks(*args, libtype='LdMod'), + } + + # these variables were set by gnulink but are not used in cyglink + try: del env['_SHLIBSONAME'] + except KeyError: pass + try: del env['_LDMODULESONAME'] + except KeyError: pass + def exists(env): return gnulink.exists(env) diff --git a/src/engine/SCons/Tool/cyglink.xml b/src/engine/SCons/Tool/cyglink.xml new file mode 100644 index 0000000..42208f1 --- /dev/null +++ b/src/engine/SCons/Tool/cyglink.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +__COPYRIGHT__ + +This file is processed by the bin/SConsDoc.py module. +See its __doc__ string for a discussion of the format. +--> + +<!DOCTYPE sconsdoc [ +<!ENTITY % scons SYSTEM '../../../../doc/scons.mod'> +%scons; +<!ENTITY % builders-mod SYSTEM '../../../../doc/generated/builders.mod'> +%builders-mod; +<!ENTITY % functions-mod SYSTEM '../../../../doc/generated/functions.mod'> +%functions-mod; +<!ENTITY % tools-mod SYSTEM '../../../../doc/generated/tools.mod'> +%tools-mod; +<!ENTITY % variables-mod SYSTEM '../../../../doc/generated/variables.mod'> +%variables-mod; +]> + +<sconsdoc xmlns="http://www.scons.org/dbxsd/v1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.scons.org/dbxsd/v1.0 http://www.scons.org/dbxsd/v1.0/scons.xsd"> + +<tool name="cyglink"> +<summary> +<para> +Set construction variables for cygwin linker/loader. +</para> +</summary> +<sets> +<item>IMPLIBPREFIX</item> +<item>IMPLIBSUFFIX</item> +<item>LDMODULEVERSIONFLAGS</item> +<item>LINKFLAGS</item> +<item>RPATHPREFIX</item> +<item>RPATHSUFFIX</item> +<item>SHLIBPREFIX</item> +<item>SHLIBSUFFIX</item> +<item>SHLIBVERSIONFLAGS</item> +<item>SHLINKCOM</item> +<item>SHLINKFLAGS</item> +<item>_LDMODULEVERSIONFLAGS</item> +<item>_SHLIBVERSIONFLAGS</item> +</sets> +</tool> + +</sconsdoc> diff --git a/src/engine/SCons/Tool/dmd.py b/src/engine/SCons/Tool/dmd.py index a7d46c6..3722936 100644 --- a/src/engine/SCons/Tool/dmd.py +++ b/src/engine/SCons/Tool/dmd.py @@ -114,7 +114,7 @@ def generate(env): env['DSHLINK'] = '$DC' env['DSHLINKFLAGS'] = SCons.Util.CLVar('$DLINKFLAGS -shared -defaultlib=libphobos2.so') - env['SHDLINKCOM'] = '$DLINK -of$TARGET $DSHLINKFLAGS $__DRPATH $SOURCES $_DLIBDIRFLAGS $_DLIBFLAGS' + env['SHDLINKCOM'] = '$DLINK -of$TARGET $DSHLINKFLAGS $__DSHLIBVERSIONFLAGS $__DRPATH $SOURCES $_DLIBDIRFLAGS $_DLIBFLAGS' env['DLIBLINKPREFIX'] = '' if env['PLATFORM'] == 'win32' else '-L-l' env['DLIBLINKSUFFIX'] = '.lib' if env['PLATFORM'] == 'win32' else '' @@ -139,6 +139,17 @@ def generate(env): env['DRPATHSUFFIX'] = '' env['_DRPATH'] = '${_concat(DRPATHPREFIX, RPATH, DRPATHSUFFIX, __env__)}' + # Support for versioned libraries + env['_DSHLIBVERSIONFLAGS'] = '$DSHLIBVERSIONFLAGS -L-soname=$_DSHLIBSONAME' + env['_DSHLIBSONAME'] = '${DShLibSonameGenerator(__env__,TARGET)}' + # NOTE: this is a quick hack, the soname will only work if there is + # c/c++ linker loaded which provides callback for the ShLibSonameGenerator + env['DShLibSonameGenerator'] = SCons.Tool.ShLibSonameGenerator + # NOTE: this is only for further reference, currently $DSHLIBVERSION does + # not work, the user must use $SHLIBVERSION + env['DSHLIBVERSION'] = '$SHLIBVERSION' + env['DSHLIBVERSIONFLAGS'] = [] + SCons.Tool.createStaticLibBuilder(env) diff --git a/src/engine/SCons/Tool/docbook/__init__.py b/src/engine/SCons/Tool/docbook/__init__.py index 26a1a95..aead43c 100644 --- a/src/engine/SCons/Tool/docbook/__init__.py +++ b/src/engine/SCons/Tool/docbook/__init__.py @@ -448,7 +448,7 @@ def DocbookEpub(env, target, source=None, *args, **kw): Ensure all the resources in the manifest are present in the OEBPS directory. """ hrefs = [] - content_file = os.path.join(source[0].abspath, 'content.opf') + content_file = os.path.join(source[0].get_abspath(), 'content.opf') if not os.path.isfile(content_file): return @@ -491,9 +491,9 @@ def DocbookEpub(env, target, source=None, *args, **kw): for href in hrefs: # If the resource was not already created by DocBook XSL itself, # copy it into the OEBPS folder - referenced_file = os.path.join(source[0].abspath, href) + referenced_file = os.path.join(source[0].get_abspath(), href) if not os.path.exists(referenced_file): - shutil.copy(href, os.path.join(source[0].abspath, href)) + shutil.copy(href, os.path.join(source[0].get_abspath(), href)) # Init list of targets/sources target, source = __extend_targets_sources(target, source) diff --git a/src/engine/SCons/Tool/gdc.py b/src/engine/SCons/Tool/gdc.py index 799c3ab..32199b3 100644 --- a/src/engine/SCons/Tool/gdc.py +++ b/src/engine/SCons/Tool/gdc.py @@ -97,7 +97,7 @@ def generate(env): env['DSHLINK'] = '$DC' env['DSHLINKFLAGS'] = SCons.Util.CLVar('$DLINKFLAGS -shared') - env['SHDLINKCOM'] = '$DLINK -o $TARGET $DSHLINKFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' + env['SHDLINKCOM'] = '$DLINK -o $TARGET $DSHLINKFLAGS $__DSHLIBVERSIONFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' env['DLIB'] = 'lib' if env['PLATFORM'] == 'win32' else 'ar cr' env['DLIBCOM'] = '$DLIB $_DLIBFLAGS {0}$TARGET $SOURCES $_DLINKLIBFLAGS'.format('-c ' if env['PLATFORM'] == 'win32' else '') @@ -115,6 +115,17 @@ def generate(env): env['RPATHSUFFIX'] = '' env['_RPATH'] = '${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}' + # Support for versioned libraries + env['_DSHLIBVERSIONFLAGS'] = '$DSHLIBVERSIONFLAGS -Wl,-soname=$_DSHLIBSONAME' + env['_DSHLIBSONAME'] = '${DShLibSonameGenerator(__env__,TARGET)}' + # NOTE: this is a quick hack, the soname will only work if there is + # c/c++ linker loaded which provides callback for the ShLibSonameGenerator + env['DShLibSonameGenerator'] = SCons.Tool.ShLibSonameGenerator + # NOTE: this is only for further reference, currently $DSHLIBVERSION does + # not work, the user must use $SHLIBVERSION + env['DSHLIBVERSION'] = '$SHLIBVERSION' + env['DSHLIBVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS' + SCons.Tool.createStaticLibBuilder(env) diff --git a/src/engine/SCons/Tool/gettext.xml b/src/engine/SCons/Tool/gettext.xml index 96e467a..f9f8b81 100644 --- a/src/engine/SCons/Tool/gettext.xml +++ b/src/engine/SCons/Tool/gettext.xml @@ -27,7 +27,7 @@ See its __doc__ string for a discussion of the format. <summary> <para> This is actually a toolset, which supports internationalization and -localization of sofware being constructed with SCons. The toolset loads +localization of software being constructed with SCons. The toolset loads following tools: </para> diff --git a/src/engine/SCons/Tool/gnulink.py b/src/engine/SCons/Tool/gnulink.py index 3dc8f51..6b0d5b3 100644 --- a/src/engine/SCons/Tool/gnulink.py +++ b/src/engine/SCons/Tool/gnulink.py @@ -34,9 +34,14 @@ selection method. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import SCons.Util +import SCons.Tool +import os +import sys +import re import link + def generate(env): """Add Builders and construction variables for gnulink to an Environment.""" link.generate(env) @@ -49,6 +54,14 @@ def generate(env): env['RPATHPREFIX'] = '-Wl,-rpath=' env['RPATHSUFFIX'] = '' env['_RPATH'] = '${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}' + + # OpenBSD doesn't usually use SONAME for libraries + use_soname = not sys.platform.startswith('openbsd') + link._setup_versioned_lib_variables(env, tool = 'gnulink', use_soname = use_soname) + env['LINKCALLBACKS'] = link._versioned_lib_callbacks() + + # For backward-compatiblity with older SCons versions + env['SHLIBVERSIONFLAGS'] = SCons.Util.CLVar('-Wl,-Bsymbolic') def exists(env): # TODO: sync with link.smart_link() to choose a linker diff --git a/src/engine/SCons/Tool/gnulink.xml b/src/engine/SCons/Tool/gnulink.xml index 2a36de2..0e055c7 100644 --- a/src/engine/SCons/Tool/gnulink.xml +++ b/src/engine/SCons/Tool/gnulink.xml @@ -33,6 +33,10 @@ Set construction variables for GNU linker/loader. <item>SHLINKFLAGS</item> <item>RPATHPREFIX</item> <item>RPATHSUFFIX</item> +<item>_LDMODULESONAME</item> +<item>_SHLIBSONAME</item> +<item>LDMODULEVERSIONFLAGS</item> +<item>SHLIBVERSIONFLAGS</item> </sets> </tool> diff --git a/src/engine/SCons/Tool/install.py b/src/engine/SCons/Tool/install.py index 9f2e937..9d5db9f 100644 --- a/src/engine/SCons/Tool/install.py +++ b/src/engine/SCons/Tool/install.py @@ -38,6 +38,7 @@ import shutil import stat import SCons.Action +import SCons.Tool from SCons.Util import make_path_relative # @@ -141,98 +142,35 @@ def copyFuncVersionedLib(dest, source, env): shutil.copy2(source, dest) st = os.stat(source) os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) - versionedLibLinks(dest, source, env) + installShlibLinks(dest, source, env) return 0 -def versionedLibVersion(dest, source, env): - """Check if dest is a version shared library name. Return version, libname, & install_dir if it is.""" +def listShlibLinksToInstall(dest, source, env): + install_links = [] + source = env.arg2nodes(source) + dest = env.fs.File(dest) + install_dir = dest.get_dir() + for src in source: + symlinks = getattr(getattr(src,'attributes',None), 'shliblinks', None) + if symlinks: + for link, linktgt in symlinks: + link_base = os.path.basename(link.get_path()) + linktgt_base = os.path.basename(linktgt.get_path()) + install_link = env.fs.File(link_base, install_dir) + install_linktgt = env.fs.File(linktgt_base, install_dir) + install_links.append((install_link, install_linktgt)) + return install_links + +def installShlibLinks(dest, source, env): + """If we are installing a versioned shared library create the required links.""" Verbose = False - platform = env.subst('$PLATFORM') - if not (platform == 'posix' or platform == 'darwin' or platform == 'sunos'): - return (None, None, None) - if (hasattr(source[0], 'attributes') and - hasattr(source[0].attributes, 'shlibname')): - libname = source[0].attributes.shlibname - else: - libname = os.path.basename(str(dest)) - install_dir = os.path.dirname(str(dest)) - shlib_suffix = env.subst('$SHLIBSUFFIX') - # See if the source name is a versioned shared library, get the version number - result = False - - version_re = re.compile("[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+") - version_File = None - if platform == 'posix' or platform == 'sunos': - # handle unix names - versioned_re = re.compile(re.escape(shlib_suffix + '.') + "[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+") - result = versioned_re.findall(libname) - if result: - version_File = version_re.findall(versioned_re.findall(libname)[-1])[-1] - elif platform == 'darwin': - # handle OSX names - versioned_re = re.compile("\\.[0-9]+\\.[0-9]+\\.[0-9a-zA-Z]+" + re.escape(shlib_suffix) ) - result = versioned_re.findall(libname) - if result: - version_File = version_re.findall(versioned_re.findall(libname)[-1])[-1] - + symlinks = listShlibLinksToInstall(dest, source, env) if Verbose: - print "install: version_File ", version_File - # result is False if we did not find a versioned shared library name, so return and empty list - if not result: - return (None, libname, install_dir) - - version = None - # get version number from the environment - try: - version = env.subst('$SHLIBVERSION') - except KeyError: - version = None - - if version != version_File: - #raise SCons.Errors.UserError("SHLIBVERSION '%s' does not match the version # '%s' in the filename" % (version, version_File) ) - print "SHLIBVERSION '%s' does not match the version # '%s' in the filename, proceeding based on file name" % (version, version_File) - version = version_File - return (version, libname, install_dir) - -def versionedLibLinks(dest, source, env): - """If we are installing a versioned shared library create the required links.""" - Verbose = False - linknames = [] - version, libname, install_dir = versionedLibVersion(dest, source, env) - - if version != None: - # libname includes the version number if one was given - linknames = SCons.Tool.VersionShLibLinkNames(version,libname,env) - if Verbose: - print "versionedLibLinks: linknames ",linknames - # Here we just need the file name w/o path as the target of the link - lib_ver = libname - # make symlink of adjacent names in linknames - for count in range(len(linknames)): - linkname = linknames[count] - fulllinkname = os.path.join(install_dir, linkname) - if Verbose: - print "full link name ",fulllinkname - if count > 0: - try: - os.remove(lastlinkname) - except: - pass - os.symlink(os.path.basename(fulllinkname),lastlinkname) - if Verbose: - print "versionedLibLinks: made sym link of %s -> %s" % (lastlinkname,os.path.basename(fulllinkname)) - lastlinkname = fulllinkname - # finish chain of sym links with link to the actual library - if len(linknames)>0: - try: - os.remove(lastlinkname) - except: - pass - os.symlink(lib_ver,lastlinkname) - if Verbose: - print "versionedLibLinks: made sym link of %s -> %s" % (lib_ver,lastlinkname) + print 'installShlibLinks: symlinks=%r' % SCons.Tool.StringizeLibSymlinks(symlinks) + if symlinks: + SCons.Tool.CreateLibSymlinks(env, symlinks) return def installFunc(target, source, env): @@ -306,22 +244,11 @@ def add_versioned_targets_to_INSTALLED_FILES(target, source, env): Verbose = False _INSTALLED_FILES.extend(target) if Verbose: - print "ver lib emitter ",repr(target) - - # see if we have a versioned shared library, if so generate side effects - version, libname, install_dir = versionedLibVersion(target[0], source, env) - if version != None: - # generate list of link names - linknames = SCons.Tool.VersionShLibLinkNames(version,libname,env) - for linkname in linknames: - if Verbose: - print "make side effect of %s" % os.path.join(install_dir, linkname) - fulllinkname = os.path.join(install_dir, linkname) - env.SideEffect(fulllinkname,target[0]) - env.Clean(target[0],fulllinkname) - _INSTALLED_FILES.append(fulllinkname) - if Verbose: - print "installed list ", _INSTALLED_FILES + print "add_versioned_targets_to_INSTALLED_FILES: target=%r" % map(str, target) + + symlinks = listShlibLinksToInstall(target[0], source, env) + if symlinks: + SCons.Tool.EmitLibSymlinks(env, symlinks, target[0]) _UNIQUE_INSTALLED_FILES = None return (target, source) diff --git a/src/engine/SCons/Tool/install.xml b/src/engine/SCons/Tool/install.xml index 0aa9384..6ae3e30 100644 --- a/src/engine/SCons/Tool/install.xml +++ b/src/engine/SCons/Tool/install.xml @@ -82,20 +82,13 @@ env.InstallAs(target = ['../lib/libfoo.a', '../lib/libbar.a'], <builder name="InstallVersionedLib"> <summary> <para> -Installs a versioned shared library. The &cv-link-SHLIBVERSION; -construction variable should be defined in the environment -to confirm the version number in the library name. -If &cv-link-SHLIBVERSION; is not defined a warning will be issued -and the name of the library will be parsed to derive the version. -The symlinks appropriate to the architecture will be generated. +Installs a versioned shared library. The symlinks appropriate to the +architecture will be generated based on symlinks of the source library. </para> <example_commands> env.InstallVersionedLib(target = '/usr/local/bin/foo', source = 'libxyz.1.5.2.so') -env.InstallVersionedLib(target = '/usr/local/bin/foo', - source = 'libxyz.1.5.2.so', - SHLIBVERSION='1.5.2') </example_commands> </summary> </builder> diff --git a/src/engine/SCons/Tool/ldc.py b/src/engine/SCons/Tool/ldc.py index 8f9b117..ade95db 100644 --- a/src/engine/SCons/Tool/ldc.py +++ b/src/engine/SCons/Tool/ldc.py @@ -105,7 +105,7 @@ def generate(env): # Hack for Fedora the packages of which use the wrong name :-( if os.path.exists('/usr/lib64/libphobos-ldc.so') or os.path.exists('/usr/lib32/libphobos-ldc.so') or os.path.exists('/usr/lib/libphobos-ldc.so') : env['DSHLINKFLAGS'] = SCons.Util.CLVar('$DLINKFLAGS -shared -defaultlib=phobos-ldc') - env['SHDLINKCOM'] = '$DLINK -of=$TARGET $DSHLINKFLAGS $__DRPATH $SOURCES $_DLIBDIRFLAGS $_DLIBFLAGS' + env['SHDLINKCOM'] = '$DLINK -of=$TARGET $DSHLINKFLAGS $__DSHLIBVERSIONFLAGS $__DRPATH $SOURCES $_DLIBDIRFLAGS $_DLIBFLAGS' env['DLIBLINKPREFIX'] = '' if env['PLATFORM'] == 'win32' else '-L-l' env['DLIBLINKSUFFIX'] = '.lib' if env['PLATFORM'] == 'win32' else '' @@ -131,6 +131,17 @@ def generate(env): env['DRPATHSUFFIX'] = '' env['_DRPATH'] = '${_concat(DRPATHPREFIX, RPATH, DRPATHSUFFIX, __env__)}' + # Support for versioned libraries + env['_DSHLIBVERSIONFLAGS'] = '$DSHLIBVERSIONFLAGS -L-soname=$_DSHLIBSONAME' + env['_DSHLIBSONAME'] = '${DShLibSonameGenerator(__env__,TARGET)}' + # NOTE: this is a quick hack, the soname will only work if there is + # c/c++ linker loaded which provides callback for the ShLibSonameGenerator + env['DShLibSonameGenerator'] = SCons.Tool.ShLibSonameGenerator + # NOTE: this is only for further reference, currently $DSHLIBVERSION does + # not work, the user must use $SHLIBVERSION + env['DSHLIBVERSION'] = '$SHLIBVERSION' + env['DSHLIBVERSIONFLAGS'] = [] + SCons.Tool.createStaticLibBuilder(env) diff --git a/src/engine/SCons/Tool/link.py b/src/engine/SCons/Tool/link.py index 2624946..a4a2a4c 100644 --- a/src/engine/SCons/Tool/link.py +++ b/src/engine/SCons/Tool/link.py @@ -33,9 +33,10 @@ selection method. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +import sys import re +import os -import SCons.Defaults import SCons.Tool import SCons.Util import SCons.Warnings @@ -72,97 +73,205 @@ def smart_link(source, target, env, for_signature): return '$CXX' return '$CC' -def shlib_emitter(target, source, env): +def _lib_emitter(target, source, env, **kw): Verbose = False - platform = env.subst('$PLATFORM') + if Verbose: + print "_lib_emitter: target[0]=%r" % target[0].get_path() for tgt in target: tgt.attributes.shared = 1 + try: - # target[0] comes in as libtest.so. Add the version extensions - version = env.subst('$SHLIBVERSION') - if version: - version_names = shlib_emitter_names(target, source, env) - # mark the target with the shared libraries name, including - # the version number - target[0].attributes.shlibname = version_names[0] - shlib = env.File(version_names[0], directory=target[0].get_dir()) - target[0].attributes.shlibpath = shlib.path - for name in version_names[1:]: - env.SideEffect(name, shlib) - env.Clean(shlib, name) - if Verbose: - print "shlib_emitter: add side effect - ",name - env.Clean(shlib, target[0]) - return ([shlib], source) + symlink_generator = kw['symlink_generator'] except KeyError: - version = None + pass + else: + if Verbose: + print "_lib_emitter: symlink_generator=%r" % symlink_generator + symlinks = symlink_generator(env, target[0]) + if Verbose: + print "_lib_emitter: symlinks=%r" % symlinks + + if symlinks: + SCons.Tool.EmitLibSymlinks(env, symlinks, target[0]) + target[0].attributes.shliblinks = symlinks return (target, source) -def shlib_emitter_names(target, source, env): - """Return list of file names that are side effects for a versioned library build. The first name in the list is the new name for the target""" +def shlib_emitter(target, source, env): + return _lib_emitter(target, source, env, symlink_generator = SCons.Tool.ShLibSymlinkGenerator) + +def ldmod_emitter(target, source, env): + return _lib_emitter(target, source, env, symlink_generator = SCons.Tool.LdModSymlinkGenerator) + +# This is generic enough to be included here... +def _versioned_lib_name(env, libnode, version, prefix, suffix, prefix_generator, suffix_generator, **kw): + """For libnode='/optional/dir/libfoo.so.X.Y.Z' it returns 'libfoo.so'""" Verbose = False - platform = env.subst('$PLATFORM') - version_names = [] - try: - # target[0] comes in as libtest.so. Add the version extensions - version = env.subst('$SHLIBVERSION') - if version.count(".") != 2: - # We need a version of the form x.y.z to proceed - raise ValueError - if version: - if platform == 'posix' or platform == 'sunos': - versionparts = version.split('.') - if hasattr(target[0].attributes, 'shlibname'): - name = target[0].attributes.shlibname - else: - name = target[0].name - # generate library name with the version number - version_name = name + '.' + version - if Verbose: - print "shlib_emitter_names: target is ", version_name - print "shlib_emitter_names: side effect: ", name - # add version_name to list of names to be a Side effect - version_names.append(version_name) - if Verbose: - print "shlib_emitter_names: versionparts ",versionparts - for ver in versionparts[0:-1]: - name = name + '.' + ver - if Verbose: - print "shlib_emitter_names: side effect: ", name - # add name to list of names to be a Side effect - version_names.append(name) - elif platform == 'darwin': - shlib_suffix = env.subst('$SHLIBSUFFIX') - if hasattr(target[0].attributes, 'shlibname'): - name = target[0].attributes.shlibname - else: - name = target[0].name - # generate library name with the version number - suffix_re = re.escape(shlib_suffix) - version_name = re.sub(suffix_re, '.' + version + shlib_suffix, name) - if Verbose: - print "shlib_emitter_names: target is ", version_name - print "shlib_emitter_names: side effect: ", name - # add version_name to list of names to be a Side effect - version_names.append(version_name) - elif platform == 'cygwin': - shlib_suffix = env.subst('$SHLIBSUFFIX') - if hasattr(target[0].attributes, 'shlibname'): - name = target[0].attributes.shlibname - else: - name = target[0].name - # generate library name with the version number - suffix_re = re.escape(shlib_suffix) - version_name = re.sub(suffix_re, '-' + re.sub('\.', '-', version) + shlib_suffix, name) - if Verbose: - print "shlib_emitter_names: target is ", version_name - print "shlib_emitter_names: side effect: ", name - # add version_name to list of names to be a Side effect - version_names.append(version_name) - except KeyError: - version = None - return version_names + if Verbose: + print "_versioned_lib_name: libnode=%r" % libnode.get_path() + print "_versioned_lib_name: version=%r" % version + print "_versioned_lib_name: prefix=%r" % prefix + print "_versioned_lib_name: suffix=%r" % suffix + print "_versioned_lib_name: suffix_generator=%r" % suffix_generator + + versioned_name = os.path.basename(libnode.get_path()) + if Verbose: + print "_versioned_lib_name: versioned_name=%r" % versioned_name + + versioned_prefix = prefix_generator(env, **kw) + versioned_suffix = suffix_generator(env, **kw) + if Verbose: + print "_versioned_lib_name: versioned_prefix=%r" % versioned_prefix + print "_versioned_lib_name: versioned_suffix=%r" % versioned_suffix + + versioned_prefix_re = '^' + re.escape(versioned_prefix) + versioned_suffix_re = re.escape(versioned_suffix) + '$' + name = re.sub(versioned_prefix_re, prefix, versioned_name) + name = re.sub(versioned_suffix_re, suffix, name) + if Verbose: + print "_versioned_lib_name: name=%r" % name + return name + +def _versioned_shlib_name(env, libnode, version, prefix, suffix, **kw): + pg = SCons.Tool.ShLibPrefixGenerator + sg = SCons.Tool.ShLibSuffixGenerator + return _versioned_lib_name(env, libnode, version, prefix, suffix, pg, sg, **kw) + +def _versioned_ldmod_name(env, libnode, version, prefix, suffix, **kw): + pg = SCons.Tool.LdModPrefixGenerator + sg = SCons.Tool.LdModSuffixGenerator + return _versioned_lib_name(env, libnode, version, prefix, suffix, pg, sg, **kw) + +def _versioned_lib_suffix(env, suffix, version): + """For suffix='.so' and version='0.1.2' it returns '.so.0.1.2'""" + Verbose = False + if Verbose: + print "_versioned_lib_suffix: suffix=%r" % suffix + print "_versioned_lib_suffix: version=%r" % version + if not suffix.endswith(version): + suffix = suffix + '.' + version + if Verbose: + print "_versioned_lib_suffix: return suffix=%r" % suffix + return suffix + +def _versioned_lib_soname(env, libnode, version, prefix, suffix, name_func): + """For libnode='/optional/dir/libfoo.so.X.Y.Z' it returns 'libfoo.so.X'""" + Verbose = False + if Verbose: + print "_versioned_lib_soname: version=%r" % version + name = name_func(env, libnode, version, prefix, suffix) + if Verbose: + print "_versioned_lib_soname: name=%r" % name + major = version.split('.')[0] + soname = name + '.' + major + if Verbose: + print "_versioned_lib_soname: soname=%r" % soname + return soname + +def _versioned_shlib_soname(env, libnode, version, prefix, suffix): + return _versioned_lib_soname(env, libnode, version, prefix, suffix, _versioned_shlib_name) + +def _versioned_ldmod_soname(env, libnode, version, prefix, suffix): + return _versioned_lib_soname(env, libnode, version, prefix, suffix, _versioned_ldmod_name) + +def _versioned_lib_symlinks(env, libnode, version, prefix, suffix, name_func, soname_func): + """Generate link names that should be created for a versioned shared lirbrary. + Returns a dictionary in the form { linkname : linktarget } + """ + Verbose = False + + if Verbose: + print "_versioned_lib_symlinks: libnode=%r" % libnode.get_path() + print "_versioned_lib_symlinks: version=%r" % version + + if sys.platform.startswith('openbsd'): + # OpenBSD uses x.y shared library versioning numbering convention + # and doesn't use symlinks to backwards-compatible libraries + if Verbose: + print "_versioned_lib_symlinks: return symlinks=%r" % None + return None + + linkdir = libnode.get_dir() + if Verbose: + print "_versioned_lib_symlinks: linkdir=%r" % linkdir.get_path() + + name = name_func(env, libnode, version, prefix, suffix) + if Verbose: + print "_versioned_lib_symlinks: name=%r" % name + + soname = soname_func(env, libnode, version, prefix, suffix) + + link0 = env.fs.File(soname, linkdir) + link1 = env.fs.File(name, linkdir) + + # We create direct symlinks, not daisy-chained. + if link0 == libnode: + # This enables SHLIBVERSION without periods (e.g. SHLIBVERSION=1) + symlinks = [ (link1, libnode) ] + else: + # This handles usual SHLIBVERSION, i.e. '1.2', '1.2.3', etc. + symlinks = [ (link0, libnode), (link1, libnode) ] + + if Verbose: + print "_versioned_lib_symlinks: return symlinks=%r" % SCons.Tool.StringizeLibSymlinks(symlinks) + + return symlinks + +def _versioned_shlib_symlinks(env, libnode, version, prefix, suffix): + nf = _versioned_shlib_name + sf = _versioned_shlib_soname + return _versioned_lib_symlinks(env, libnode, version, prefix, suffix, nf, sf) + +def _versioned_ldmod_symlinks(env, libnode, version, prefix, suffix): + nf = _versioned_ldmod_name + sf = _versioned_ldmod_soname + return _versioned_lib_symlinks(env, libnode, version, prefix, suffix, nf, sf) + +def _versioned_lib_callbacks(): + return { + 'VersionedShLibSuffix' : _versioned_lib_suffix, + 'VersionedLdModSuffix' : _versioned_lib_suffix, + 'VersionedShLibSymlinks' : _versioned_shlib_symlinks, + 'VersionedLdModSymlinks' : _versioned_ldmod_symlinks, + 'VersionedShLibName' : _versioned_shlib_name, + 'VersionedLdModName' : _versioned_ldmod_name, + 'VersionedShLibSoname' : _versioned_shlib_soname, + 'VersionedLdModSoname' : _versioned_ldmod_soname, + }.copy() + +# Setup all variables required by the versioning machinery +def _setup_versioned_lib_variables(env, **kw): + + tool = None + try: tool = kw['tool'] + except KeyError: pass + + use_soname = False + try: use_soname = kw['use_soname'] + except KeyError: pass + + # The $_SHLIBVERSIONFLAGS define extra commandline flags used when + # building VERSIONED shared libraries. It's always set, but used only + # when VERSIONED library is built (see __SHLIBVERSIONFLAGS in SCons/Defaults.py). + if use_soname: + # If the linker uses SONAME, then we need this little automata + if tool == 'sunlink': + env['_SHLIBVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS -h $_SHLIBSONAME' + env['_LDMODULEVERSIONFLAGS'] = '$LDMODULEVERSIONFLAGS -h $_LDMODULESONAME' + else: + env['_SHLIBVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS -Wl,-soname=$_SHLIBSONAME' + env['_LDMODULEVERSIONFLAGS'] = '$LDMODULEVERSIONFLAGS -Wl,-soname=$_LDMODULESONAME' + env['_SHLIBSONAME'] = '${ShLibSonameGenerator(__env__,TARGET)}' + env['_LDMODULESONAME'] = '${LdModSonameGenerator(__env__,TARGET)}' + env['ShLibSonameGenerator'] = SCons.Tool.ShLibSonameGenerator + env['LdModSonameGenerator'] = SCons.Tool.LdModSonameGenerator + else: + env['_SHLIBVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS' + env['_LDMODULEVERSIONFLAGS'] = '$LDMODULEVERSIONFLAGS' + + # LDOMDULVERSIONFLAGS should always default to $SHLIBVERSIONFLAGS + env['LDMODULEVERSIONFLAGS'] = '$SHLIBVERSIONFLAGS' + def generate(env): """Add Builders and construction variables for gnulink to an Environment.""" @@ -171,7 +280,7 @@ def generate(env): env['SHLINK'] = '$LINK' env['SHLINKFLAGS'] = SCons.Util.CLVar('$LINKFLAGS -shared') - env['SHLINKCOM'] = '$SHLINK -o $TARGET $SHLINKFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' + env['SHLINKCOM'] = '$SHLINK -o $TARGET $SHLINKFLAGS $__SHLIBVERSIONFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' # don't set up the emitter, cause AppendUnique will generate a list # starting with None :-( env.Append(SHLIBEMITTER = [shlib_emitter]) @@ -196,15 +305,13 @@ def generate(env): # setting them the same means that LoadableModule works everywhere. SCons.Tool.createLoadableModuleBuilder(env) env['LDMODULE'] = '$SHLINK' - # don't set up the emitter, cause AppendUnique will generate a list - # starting with None :-( - env.Append(LDMODULEEMITTER='$SHLIBEMITTER') + env.Append(LDMODULEEMITTER = [ldmod_emitter]) env['LDMODULEPREFIX'] = '$SHLIBPREFIX' env['LDMODULESUFFIX'] = '$SHLIBSUFFIX' env['LDMODULEFLAGS'] = '$SHLINKFLAGS' - env['LDMODULECOM'] = '$LDMODULE -o $TARGET $LDMODULEFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' - - + env['LDMODULECOM'] = '$LDMODULE -o $TARGET $LDMODULEFLAGS $__LDMODULEVERSIONFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' + env['LDMODULEVERSION'] = '$SHLIBVERSION' + env['LDMODULENOVERSIONSYMLINKS'] = '$SHLIBNOVERSIONSYMLINKS' def exists(env): # This module isn't really a Tool on its own, it's common logic for diff --git a/src/engine/SCons/Tool/link.xml b/src/engine/SCons/Tool/link.xml index d58b9e2..2f913fe 100644 --- a/src/engine/SCons/Tool/link.xml +++ b/src/engine/SCons/Tool/link.xml @@ -41,11 +41,16 @@ Sets construction variables for generic POSIX linkers. <item>LIBLINKPREFIX</item> <item>LIBLINKSUFFIX</item> <item>SHLIBSUFFIX</item> +<item>__SHLIBVERSIONFLAGS</item> <item>LDMODULE</item> <item>LDMODULEPREFIX</item> <item>LDMODULESUFFIX</item> <item>LDMODULEFLAGS</item> <item>LDMODULECOM</item> +<item>LDMODULEVERSION</item> +<item>LDMODULENOVERSIONSYMLINKS</item> +<item>LDMODULEVERSIONFLAGS</item> +<item>__LDMODULEVERSIONFLAGS</item> </sets> <uses> <item>SHLINKCOMSTR</item> @@ -54,6 +59,105 @@ Sets construction variables for generic POSIX linkers. </uses> </tool> +<cvar name="__LDMODULEVERSIONFLAGS"> +<summary> +<para> +This construction variable automatically introduces &cv-link-_LDMODULEVERSIONFLAGS; +if &cv-link-LDMODULEVERSION; is set. Othervise it evaluates to an empty string. +</para> +</summary> +</cvar> + +<cvar name="__SHLIBVERSIONFLAGS"> +<summary> +<para> +This construction variable automatically introduces &cv-link-_SHLIBVERSIONFLAGS; +if &cv-link-SHLIBVERSION; is set. Othervise it evaluates to an empty string. +</para> +</summary> +</cvar> + +<cvar name="_LDMODULESONAME"> +<summary> +<para> +A macro that automatically generates loadable module's SONAME based on $TARGET, +$LDMODULEVERSION and $LDMODULESUFFIX. Used by &b-link-LoadableModule; builder +when the linker tool supports SONAME (e.g. &t-link-gnulink;). +</para> +</summary> +</cvar> + +<cvar name="_LDMODULEVERSIONFLAGS"> +<summary> +<para> +This macro automatically introduces extra flags to &cv-link-LDMODULECOM; when +building versioned &b-link-LoadableModule; (that is when +&cv-link-LDMODULEVERSION; is set). <literal>_LDMODULEVERSIONFLAGS</literal> +usually adds &cv-link-SHLIBVERSIONFLAGS; and some extra dynamically generated +options (such as <literal>-Wl,-soname=$_LDMODULESONAME</literal>). It is unused +by plain (unversioned) loadable modules. +</para> +</summary> +</cvar> + +<cvar name="_SHLIBVERSIONFLAGS"> +<summary> +<para> +This macro automatically introduces extra flags to &cv-link-SHLINKCOM; when +building versioned &b-link-SharedLibrary; (that is when &cv-link-SHLIBVERSION; +is set). <literal>_SHLIBVERSIONFLAGS</literal> usually adds &cv-link-SHLIBVERSIONFLAGS; +and some extra dynamically generated options (such as +<literal>-Wl,-soname=$_SHLIBSONAME</literal>. It is unused by "plain" +(unversioned) shared libraries. +</para> +</summary> +</cvar> + +<cvar name="_SHLIBSONAME"> +<summary> +<para> +A macro that automatically generates shared library's SONAME based on $TARGET, +$SHLIBVERSION and $SHLIBSUFFIX. Used by &b-link-SharedLibrary; builder when +the linker tool supports SONAME (e.g. &t-link-gnulink;). +</para> +</summary> +</cvar> + +<cvar name="IMPLIBPREFIX"> +<summary> +<para> +The prefix used for import library names. For example, cygwin uses import +libraries (<literal>libfoo.dll.a</literal>) in pair with dynamic libraries +(<literal>cygfoo.dll</literal>). The &t-link-cyglink; linker sets +&cv-link-IMPLIBPREFIX; to <literal>'lib'</literal> and &cv-link-SHLIBPREFIX; +to <literal>'cyg'</literal>. +</para> +</summary> +</cvar> + +<cvar name="IMPLIBSUFFIX"> +<summary> +<para> +The suffix used for import library names. For example, cygwin uses import +libraries (<literal>libfoo.dll.a</literal>) in pair with dynamic libraries +(<literal>cygfoo.dll</literal>). The &t-link-cyglink; linker sets +&cv-link-IMPLIBSUFFIX; to <literal>'.dll.a'</literal> and &cv-link-SHLIBSUFFIX; +to <literal>'.dll'</literal>. +</para> +</summary> +</cvar> + +<cvar name="IMPLIBNOVERSIONSYMLINKS"> +<summary> +<para> +Used to override &cv-link-SHLIBNOVERSIONSYMLINKS;/&cv-link-LDMODULENOVERSIONSYMLINKS; when +creating versioned import library for a shared library/loadable module. If not defined, +then &cv-link-SHLIBNOVERSIONSYMLINKS;/&cv-link-LDMODULENOVERSIONSYMLINKS; is used to determine +whether to disable symlink generation or not. +</para> +</summary> +</cvar> + <cvar name="LDMODULE"> <summary> <para> @@ -92,6 +196,15 @@ General user options passed to the linker for building loadable modules. </summary> </cvar> +<cvar name="LDMODULENOVERSIONSYMLINKS"> +<summary> +<para> +Instructs the &b-link-LoadableModule; builder to not automatically create symlinks +for versioned modules. Defaults to <literal>$SHLIBNOVERSIONSYMLINKS</literal> +</para> +</summary> +</cvar> + <cvar name="LDMODULEPREFIX"> <summary> <para> @@ -114,6 +227,16 @@ the same as $SHLIBSUFFIX. </summary> </cvar> +<cvar name="LDMODULEVERSIONFLAGS"> +<summary> +<para> +Extra flags added to &cv-link-LDMODULECOM; when building versioned +&b-link-LoadableModule;. These flags are only used when &cv-link-LDMODULEVERSION; is +set. +</para> +</summary> +</cvar> + <cvar name="LINK"> <summary> <para> @@ -169,6 +292,25 @@ for the variable that expands to library search path options. </summary> </cvar> +<cvar name="SHLIBNOVERSIONSYMLINKS"> +<summary> +<para> +Instructs the &b-link-SharedLibrary; builder to not create symlinks for versioned +shared libraries. +</para> +</summary> +</cvar> + +<cvar name="SHLIBVERSIONFLAGS"> +<summary> +<para> +Extra flags added to &cv-link-SHLINKCOM; when building versioned +&b-link-SharedLibrary;. These flags are only used when &cv-link-SHLIBVERSION; is +set. +</para> +</summary> +</cvar> + <cvar name="SHLINK"> <summary> <para> @@ -223,6 +365,18 @@ for the variable that expands to library search path options. </summary> </cvar> +<cvar name="SONAME"> +<summary> +<para> +Variable used to hard-code SONAME for versioned shared library/loadable module. +<example_commands> +env.SharedLibrary('test', 'test.c', SHLIBVERSION='0.1.2', SONAME='libtest.so.2') +</example_commands> +The variable is used, for example, by &t-link-gnulink; linker tool. +</para> +</summary> +</cvar> + <cvar name="STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME"> <summary> <para> diff --git a/src/engine/SCons/Tool/linkloc.py b/src/engine/SCons/Tool/linkloc.py index 9c58561..bd643f7 100644 --- a/src/engine/SCons/Tool/linkloc.py +++ b/src/engine/SCons/Tool/linkloc.py @@ -86,6 +86,7 @@ def generate(env): env['SHLINKFLAGS'] = SCons.Util.CLVar('$LINKFLAGS') env['SHLINKCOM'] = '${SUBST_CMD_FILE("$SHLINK $SHLINKFLAGS $_LIBDIRFLAGS $_LIBFLAGS -dll $TARGET $SOURCES")}' env['SHLIBEMITTER']= None + env['LDMODULEEMITTER']= None env['LINK'] = "linkloc" env['LINKFLAGS'] = SCons.Util.CLVar('') env['LINKCOM'] = '${SUBST_CMD_FILE("$LINK $LINKFLAGS $_LIBDIRFLAGS $_LIBFLAGS -exe $TARGET $SOURCES")}' diff --git a/src/engine/SCons/Tool/midl.xml b/src/engine/SCons/Tool/midl.xml index 1420978..efd83cc 100644 --- a/src/engine/SCons/Tool/midl.xml +++ b/src/engine/SCons/Tool/midl.xml @@ -44,7 +44,7 @@ Sets construction variables for the Microsoft IDL compiler. <para> Builds a Windows type library (<filename>.tlb</filename>) file from an input IDL file (<filename>.idl</filename>). -In addition, it will build the associated inteface stub and +In addition, it will build the associated interface stub and proxy source files, naming them according to the base name of the <filename>.idl</filename> file. For example, diff --git a/src/engine/SCons/Tool/mingw.py b/src/engine/SCons/Tool/mingw.py index 601ec3b..948ebe5 100644 --- a/src/engine/SCons/Tool/mingw.py +++ b/src/engine/SCons/Tool/mingw.py @@ -146,6 +146,7 @@ def generate(env): env['SHLINKCOM'] = shlib_action env['LDMODULECOM'] = shlib_action env.Append(SHLIBEMITTER = [shlib_emitter]) + env.Append(LDMODULEEMITTER = [shlib_emitter]) env['AS'] = 'as' env['WIN32DEFPREFIX'] = '' diff --git a/src/engine/SCons/Tool/msgmerge.xml b/src/engine/SCons/Tool/msgmerge.xml index 2bfc6df..139b21c 100644 --- a/src/engine/SCons/Tool/msgmerge.xml +++ b/src/engine/SCons/Tool/msgmerge.xml @@ -64,7 +64,7 @@ Target nodes defined through &b-POUpdate; are not built by default (they're <literal>Ignore</literal>d from <literal>'.'</literal> node). Instead, they are added automatically to special <literal>Alias</literal> (<literal>'po-update'</literal> by default). The alias name may be changed -through the &cv-link-POUPDATE_ALIAS; construction variable. You can easilly +through the &cv-link-POUPDATE_ALIAS; construction variable. You can easily update <literal>PO</literal> files in your project by <command>scons po-update</command>. </para> diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py index 827161e..1390c20 100644 --- a/src/engine/SCons/Tool/mslink.py +++ b/src/engine/SCons/Tool/mslink.py @@ -216,7 +216,7 @@ def embedManifestDllCheck(target, source, env): """Function run by embedManifestDllCheckAction to check for existence of manifest and other conditions, and embed the manifest by calling embedManifestDllAction if so.""" if env.get('WINDOWS_EMBED_MANIFEST', 0): - manifestSrc = target[0].abspath + '.manifest' + manifestSrc = target[0].get_abspath() + '.manifest' if os.path.exists(manifestSrc): ret = (embedManifestDllAction) ([target[0]],None,env) if ret: @@ -230,7 +230,7 @@ def embedManifestExeCheck(target, source, env): """Function run by embedManifestExeCheckAction to check for existence of manifest and other conditions, and embed the manifest by calling embedManifestExeAction if so.""" if env.get('WINDOWS_EMBED_MANIFEST', 0): - manifestSrc = target[0].abspath + '.manifest' + manifestSrc = target[0].get_abspath() + '.manifest' if os.path.exists(manifestSrc): ret = (embedManifestExeAction) ([target[0]],None,env) if ret: @@ -263,6 +263,7 @@ def generate(env): env['_SHLINK_SOURCES'] = windowsShlinkSources env['SHLINKCOM'] = compositeShLinkAction env.Append(SHLIBEMITTER = [windowsLibEmitter]) + env.Append(LDMODULEEMITTER = [windowsLibEmitter]) env['LINK'] = 'link' env['LINKFLAGS'] = SCons.Util.CLVar('/nologo') env['_PDB'] = pdbGenerator diff --git a/src/engine/SCons/Tool/msvc.xml b/src/engine/SCons/Tool/msvc.xml index 793784f..2b4619e 100644 --- a/src/engine/SCons/Tool/msvc.xml +++ b/src/engine/SCons/Tool/msvc.xml @@ -86,7 +86,7 @@ file as the second element. Normally the object file is ignored. This builder method is only provided when Microsoft Visual C++ is being used as the compiler. The PCH builder method is generally used in -conjuction with the PCH construction variable to force object files to use +conjunction with the PCH construction variable to force object files to use the precompiled header: </para> diff --git a/src/engine/SCons/Tool/msvs.py b/src/engine/SCons/Tool/msvs.py index d00413d..355eeed 100644 --- a/src/engine/SCons/Tool/msvs.py +++ b/src/engine/SCons/Tool/msvs.py @@ -1535,7 +1535,9 @@ class _GenerateV7DSW(_DSWGenerator): def PrintSolution(self): """Writes a solution file""" self.file.write('Microsoft Visual Studio Solution File, Format Version %s\n' % self.versionstr) - if self.version_num >= 11.0: + if self.version_num >= 12.0: + self.file.write('# Visual Studio 14\n') + elif self.version_num >= 11.0: self.file.write('# Visual Studio 11\n') elif self.version_num >= 10.0: self.file.write('# Visual Studio 2010\n') @@ -1981,7 +1983,7 @@ def generate(env): env['MSVSSCONSCRIPT'] = default_MSVS_SConscript env['MSVSSCONS'] = '"%s" -c "%s"' % (python_executable, getExecScriptMain(env)) - env['MSVSSCONSFLAGS'] = '-C "${MSVSSCONSCRIPT.dir.abspath}" -f ${MSVSSCONSCRIPT.name}' + env['MSVSSCONSFLAGS'] = '-C "${MSVSSCONSCRIPT.dir.get_abspath()}" -f ${MSVSSCONSCRIPT.name}' env['MSVSSCONSCOM'] = '$MSVSSCONS $MSVSSCONSFLAGS' env['MSVSBUILDCOM'] = '$MSVSSCONSCOM "$MSVSBUILDTARGET"' env['MSVSREBUILDCOM'] = '$MSVSSCONSCOM "$MSVSBUILDTARGET"' diff --git a/src/engine/SCons/Tool/mwld.py b/src/engine/SCons/Tool/mwld.py index e762d55..2067660 100644 --- a/src/engine/SCons/Tool/mwld.py +++ b/src/engine/SCons/Tool/mwld.py @@ -56,6 +56,7 @@ def generate(env): env['SHLINKFLAGS'] = '$LINKFLAGS' env['SHLINKCOM'] = shlib_action env['SHLIBEMITTER']= shlib_emitter + env['LDMODULEEMITTER']= shlib_emitter def exists(env): diff --git a/src/engine/SCons/Tool/packaging/__init__.py b/src/engine/SCons/Tool/packaging/__init__.py index 95311a2..1a979ab 100644 --- a/src/engine/SCons/Tool/packaging/__init__.py +++ b/src/engine/SCons/Tool/packaging/__init__.py @@ -80,7 +80,7 @@ def Tag(env, target, source, *more_tags, **kw_tags): #if not k.startswith('PACKAGING_'): if k[:10] != 'PACKAGING_': k='PACKAGING_'+k - setattr(t, k, v) + t.Tag(k, v) def Package(env, target=None, source=None, **kw): """ Entry point for the package tool. @@ -235,9 +235,11 @@ def copy_attr(f1, f2): #pattrs = [x for x in dir(f1) if not hasattr(f2, x) and\ # x.startswith('PACKAGING_')] copyit = lambda x: not hasattr(f2, x) and x[:10] == 'PACKAGING_' - pattrs = list(filter(copyit, dir(f1))) - for attr in pattrs: - setattr(f2, attr, getattr(f1, attr)) + if f1._tags: + pattrs = list(filter(copyit, f1._tags)) + for attr in pattrs: + f2.Tag(attr, f1.GetTag(attr)) + def putintopackageroot(target, source, env, pkgroot, honor_install_location=1): """ Uses the CopyAs builder to copy all source files to the directory given in pkgroot. @@ -262,9 +264,9 @@ def putintopackageroot(target, source, env, pkgroot, honor_install_location=1): if file.is_under(pkgroot): new_source.append(file) else: - if hasattr(file, 'PACKAGING_INSTALL_LOCATION') and\ + if file.GetTag('PACKAGING_INSTALL_LOCATION') and\ honor_install_location: - new_name=make_path_relative(file.PACKAGING_INSTALL_LOCATION) + new_name=make_path_relative(file.GetTag('PACKAGING_INSTALL_LOCATION')) else: new_name=make_path_relative(file.get_path()) @@ -301,7 +303,7 @@ def stripinstallbuilder(target, source, env): for ss in s.sources: n_source.append(ss) copy_attr(s, ss) - setattr(ss, 'PACKAGING_INSTALL_LOCATION', s.get_path()) + ss.Tag('PACKAGING_INSTALL_LOCATION', s.get_path()) return (target, n_source) diff --git a/src/engine/SCons/Tool/packaging/ipk.py b/src/engine/SCons/Tool/packaging/ipk.py index 6549445..84f4e20 100644 --- a/src/engine/SCons/Tool/packaging/ipk.py +++ b/src/engine/SCons/Tool/packaging/ipk.py @@ -120,7 +120,7 @@ def build_specfiles(source, target, env): return opened_files[needle] except KeyError: file=filter(lambda x: x.get_path().rfind(needle)!=-1, haystack)[0] - opened_files[needle]=open(file.abspath, 'w') + opened_files[needle]=open(file.get_abspath(), 'w') return opened_files[needle] control_file=open_file('control', target) diff --git a/src/engine/SCons/Tool/packaging/msi.py b/src/engine/SCons/Tool/packaging/msi.py index fe78c9c..172038f 100644 --- a/src/engine/SCons/Tool/packaging/msi.py +++ b/src/engine/SCons/Tool/packaging/msi.py @@ -189,7 +189,7 @@ def build_wxsfile(target, source, env): """ compiles a .wxs file from the keywords given in env['msi_spec'] and by analyzing the tree of source nodes and their tags. """ - file = open(target[0].abspath, 'w') + file = open(target[0].get_abspath(), 'w') try: # Create a document with the Wix root tag diff --git a/src/engine/SCons/Tool/packaging/rpm.py b/src/engine/SCons/Tool/packaging/rpm.py index 2bc3063..92977d0 100644 --- a/src/engine/SCons/Tool/packaging/rpm.py +++ b/src/engine/SCons/Tool/packaging/rpm.py @@ -130,8 +130,7 @@ def build_specfile(target, source, env): """ Builds a RPM specfile from a dictionary with string metadata and by analyzing a tree of nodes. """ - file = open(target[0].abspath, 'w') - str = "" + file = open(target[0].get_abspath(), 'w') try: file.write( build_specfile_header(env) ) @@ -169,7 +168,7 @@ def build_specfile_sections(spec): 'X_RPM_POSTUNINSTALL' : '%%postun\n%s\n\n', 'X_RPM_VERIFY' : '%%verify\n%s\n\n', - # These are for internal use but could possibly be overriden + # These are for internal use but could possibly be overridden 'X_RPM_PREP' : '%%prep\n%s\n\n', 'X_RPM_BUILD' : '%%build\n%s\n\n', 'X_RPM_INSTALL' : '%%install\n%s\n\n', @@ -279,7 +278,9 @@ def build_specfile_filesection(spec, files): tags = {} for k in supported_tags.keys(): try: - tags[k]=getattr(file, k) + v = file.GetTag(k) + if v: + tags[k] = v except AttributeError: pass @@ -287,7 +288,7 @@ def build_specfile_filesection(spec, files): str = str + SimpleTagCompiler(supported_tags, mandatory=0).compile( tags ) str = str + ' ' - str = str + file.PACKAGING_INSTALL_LOCATION + str = str + file.GetTag('PACKAGING_INSTALL_LOCATION') str = str + '\n\n' return str diff --git a/src/engine/SCons/Tool/qt.py b/src/engine/SCons/Tool/qt.py index 716c7d5..7bc0ef6 100644 --- a/src/engine/SCons/Tool/qt.py +++ b/src/engine/SCons/Tool/qt.py @@ -320,6 +320,7 @@ def generate(env): # correctly later by our emitter. env.AppendUnique(PROGEMITTER =[AutomocStatic], SHLIBEMITTER=[AutomocShared], + LDMODULEEMITTER=[AutomocShared], LIBEMITTER =[AutomocStatic], # Of course, we need to link against the qt libraries CPPPATH=["$QT_CPPPATH"], diff --git a/src/engine/SCons/Tool/rpm.py b/src/engine/SCons/Tool/rpm.py index 1f6eafe..b7d65a8 100644 --- a/src/engine/SCons/Tool/rpm.py +++ b/src/engine/SCons/Tool/rpm.py @@ -51,11 +51,11 @@ def get_cmd(source, env): if SCons.Util.is_List(source): tar_file_with_included_specfile = source[0] return "%s %s %s"%(env['RPM'], env['RPMFLAGS'], - tar_file_with_included_specfile.abspath ) + tar_file_with_included_specfile.get_abspath() ) def build_rpm(target, source, env): # create a temporary rpm build root. - tmpdir = os.path.join( os.path.dirname( target[0].abspath ), 'rpmtemp' ) + tmpdir = os.path.join( os.path.dirname( target[0].get_abspath() ), 'rpmtemp' ) if os.path.exists(tmpdir): shutil.rmtree(tmpdir) @@ -87,7 +87,7 @@ def build_rpm(target, source, env): expected = os.path.basename(input.get_path()) assert expected == rpm_output, "got %s but expected %s" % (rpm_output, expected) - shutil.copy( output, input.abspath ) + shutil.copy( output, input.get_abspath() ) # cleanup before leaving. diff --git a/src/engine/SCons/Tool/sunar.py b/src/engine/SCons/Tool/sunar.py index 779414f..eb58457 100644 --- a/src/engine/SCons/Tool/sunar.py +++ b/src/engine/SCons/Tool/sunar.py @@ -51,9 +51,6 @@ def generate(env): env['ARFLAGS'] = SCons.Util.CLVar('r') env['ARCOM'] = '$AR $ARFLAGS $TARGET $SOURCES' - env['SHLINK'] = '$LINK' - env['SHLINKFLAGS'] = SCons.Util.CLVar('$LINKFLAGS -G') - env['SHLINKCOM'] = '$SHLINK $SHLINKFLAGS -o $TARGET $SOURCES $_LIBDIRFLAGS $_LIBFLAGS' env['LIBPREFIX'] = 'lib' env['LIBSUFFIX'] = '.a' diff --git a/src/engine/SCons/Tool/sunar.xml b/src/engine/SCons/Tool/sunar.xml index 65f0c9e..f875217 100644 --- a/src/engine/SCons/Tool/sunar.xml +++ b/src/engine/SCons/Tool/sunar.xml @@ -33,15 +33,11 @@ Sets construction variables for the Sun library archiver. <item>AR</item> <item>ARFLAGS</item> <item>ARCOM</item> -<item>SHLINK</item> -<item>SHLINKFLAGS</item> -<item>SHLINKCOM</item> <item>LIBPREFIX</item> <item>LIBSUFFIX</item> </sets> <uses> <item>ARCOMSTR</item> -<item>SHLINKCOMSTR</item> </uses> </tool> diff --git a/src/engine/SCons/Tool/sunlink.py b/src/engine/SCons/Tool/sunlink.py index 5996a30..680af03 100644 --- a/src/engine/SCons/Tool/sunlink.py +++ b/src/engine/SCons/Tool/sunlink.py @@ -66,6 +66,10 @@ def generate(env): env['RPATHSUFFIX'] = '' env['_RPATH'] = '${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}' + # Support for versioned libraries + link._setup_versioned_lib_variables(env, tool = 'sunlink', use_soname = True) + env['LINKCALLBACKS'] = link._versioned_lib_callbacks() + def exists(env): return ccLinker diff --git a/src/engine/SCons/Tool/swig.py b/src/engine/SCons/Tool/swig.py index f166174..a315182 100644 --- a/src/engine/SCons/Tool/swig.py +++ b/src/engine/SCons/Tool/swig.py @@ -42,6 +42,7 @@ import SCons.Defaults import SCons.Scanner import SCons.Tool import SCons.Util +import SCons.Node SwigAction = SCons.Action.Action('$SWIGCOM', '$SWIGCOMSTR') @@ -117,9 +118,13 @@ def _swigEmitter(target, source, env): if outdir: java_files = [os.path.join(outdir, j) for j in java_files] java_files = list(map(env.fs.File, java_files)) + def t_from_s(t, p, s, x): + return t.dir + tsm = SCons.Node._target_from_source_map + tkey = len(tsm) + tsm[tkey] = t_from_s for jf in java_files: - t_from_s = lambda t, p, s, x: t.dir - SCons.Util.AddMethod(jf, t_from_s, 'target_from_source') + jf._func_target_from_source = tkey target.extend(java_files) return (target, source) diff --git a/src/engine/SCons/Tool/swig.xml b/src/engine/SCons/Tool/swig.xml index 2b817dc..1160804 100644 --- a/src/engine/SCons/Tool/swig.xml +++ b/src/engine/SCons/Tool/swig.xml @@ -202,9 +202,7 @@ and translated into the The list of directories that the scripting language wrapper and interface generate will search for included files. The SWIG implicit dependency scanner will search these -directories for include files. -The default is to use the same path -specified as &cv-CPPPATH;. +directories for include files. The default value is an empty list. </para> <para> @@ -251,7 +249,7 @@ include &cv-_SWIGINCFLAGS;: </para> <example_commands> -env = Environment(SWIGCOM="my_swig -o $TARGET $_SWIGINCFLAGS $SORUCES") +env = Environment(SWIGCOM="my_swig -o $TARGET $_SWIGINCFLAGS $SOURCES") </example_commands> </summary> </cvar> diff --git a/src/engine/SCons/Tool/xgettext.xml b/src/engine/SCons/Tool/xgettext.xml index 6e28a9d..380f92a 100644 --- a/src/engine/SCons/Tool/xgettext.xml +++ b/src/engine/SCons/Tool/xgettext.xml @@ -108,7 +108,7 @@ the results shall be as the comments above say. <emphasis>Example 2.</emphasis> The &b-POTUpdate; builder may be used with no target specified, in which case default target <filename>messages.pot</filename> will be used. The -default target may also be overriden by setting &cv-link-POTDOMAIN; construction +default target may also be overridden by setting &cv-link-POTDOMAIN; construction variable or providing it as an override to &b-POTUpdate; builder: </para> <example_commands> diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py index 6dd64ec..4890ba2 100644 --- a/src/engine/SCons/Util.py +++ b/src/engine/SCons/Util.py @@ -264,10 +264,10 @@ def print_tree(root, child_func, prune=0, showtags=0, margin=[0], visited={}): children = child_func(root) if prune and rname in visited and children: - sys.stdout.write(''.join(tags + margins + ['+-[', rname, ']']) + '\n') + sys.stdout.write(''.join(tags + margins + ['+-[', rname, ']']) + u'\n') return - sys.stdout.write(''.join(tags + margins + ['+-', rname]) + '\n') + sys.stdout.write(''.join(tags + margins + ['+-', rname]) + u'\n') visited[rname] = 1 @@ -992,7 +992,7 @@ class Selector(OrderedDict): def __call__(self, env, source, ext=None): if ext is None: try: - ext = source[0].suffix + ext = source[0].get_suffix() except IndexError: ext = "" try: diff --git a/src/engine/SCons/UtilTests.py b/src/engine/SCons/UtilTests.py index b0c15c5..795bc46 100644 --- a/src/engine/SCons/UtilTests.py +++ b/src/engine/SCons/UtilTests.py @@ -591,10 +591,11 @@ class UtilTestCase(unittest.TestCase): class MyNode(object): def __init__(self, name): self.name = name - self.suffix = os.path.splitext(name)[1] def __str__(self): return self.name + def get_suffix(self): + return os.path.splitext(self.name)[1] s = Selector({'a' : 'AAA', 'b' : 'BBB'}) assert s['a'] == 'AAA', s['a'] diff --git a/src/script/scons.py b/src/script/scons.py index 0c7b44c..e522f57 100644 --- a/src/script/scons.py +++ b/src/script/scons.py @@ -191,7 +191,7 @@ if __name__ == "__main__": except: print("Import failed. Unable to find SCons files in:") for path in libs: - print " %s" % path + print(" %s" % path) raise # this does all the work, and calls sys.exit diff --git a/src/script/sconsign.py b/src/script/sconsign.py index e5e9d4f..ef32a93 100644 --- a/src/script/sconsign.py +++ b/src/script/sconsign.py @@ -278,7 +278,7 @@ def field(name, entry, verbose=Verbose): def nodeinfo_raw(name, ninfo, prefix=""): # This just formats the dictionary, which we would normally use str() # to do, except that we want the keys sorted for deterministic output. - d = ninfo.__dict__ + d = ninfo.__getstate__() try: keys = ninfo.field_list + ['_version_id'] except AttributeError: |