summaryrefslogtreecommitdiffstats
path: root/SCons/CacheDir.py
diff options
context:
space:
mode:
Diffstat (limited to 'SCons/CacheDir.py')
-rw-r--r--SCons/CacheDir.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/SCons/CacheDir.py b/SCons/CacheDir.py
new file mode 100644
index 0000000..9982555
--- /dev/null
+++ b/SCons/CacheDir.py
@@ -0,0 +1,293 @@
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+__doc__ = """
+CacheDir support
+"""
+
+import json
+import os
+import stat
+import sys
+
+import SCons
+import SCons.Action
+import SCons.Warnings
+
+cache_enabled = True
+cache_debug = False
+cache_force = False
+cache_show = False
+cache_readonly = False
+
+def CacheRetrieveFunc(target, source, env):
+ t = target[0]
+ fs = t.fs
+ cd = env.get_CacheDir()
+ cd.requests += 1
+ cachedir, cachefile = cd.cachepath(t)
+ if not fs.exists(cachefile):
+ cd.CacheDebug('CacheRetrieve(%s): %s not in cache\n', t, cachefile)
+ return 1
+ cd.hits += 1
+ 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.get_internal_path())
+ else:
+ env.copy_from_cache(cachefile, t.get_internal_path())
+ try:
+ os.utime(cachefile, None)
+ except OSError:
+ pass
+ st = fs.stat(cachefile)
+ fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
+ return 0
+
+def CacheRetrieveString(target, source, env):
+ t = target[0]
+ fs = t.fs
+ cd = env.get_CacheDir()
+ cachedir, cachefile = cd.cachepath(t)
+ if t.fs.exists(cachefile):
+ return "Retrieved `%s' from cache" % t.get_internal_path()
+ return None
+
+CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
+
+CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
+
+def CachePushFunc(target, source, env):
+ if cache_readonly:
+ return
+
+ t = target[0]
+ if t.nocache:
+ return
+ fs = t.fs
+ cd = env.get_CacheDir()
+ cachedir, cachefile = cd.cachepath(t)
+ if fs.exists(cachefile):
+ # Don't bother copying it if it's already there. Note that
+ # usually this "shouldn't happen" because if the file already
+ # existed in cache, we'd have retrieved the file from there,
+ # not built it. This can happen, though, in a race, if some
+ # other person running the same build pushes their copy to
+ # the cache after we decide we need to build it but before our
+ # build completes.
+ cd.CacheDebug('CachePush(%s): %s already exists in cache\n', t, cachefile)
+ return
+
+ cd.CacheDebug('CachePush(%s): pushing to %s\n', t, cachefile)
+
+ tempfile = cachefile+'.tmp'+str(os.getpid())
+ errfmt = "Unable to copy %s to cache. Cache file is %s"
+
+ if not fs.isdir(cachedir):
+ try:
+ fs.makedirs(cachedir)
+ except EnvironmentError:
+ # We may have received an exception because another process
+ # has beaten us creating the directory.
+ if not fs.isdir(cachedir):
+ msg = errfmt % (str(target), cachefile)
+ raise SCons.Errors.SConsEnvironmentError(msg)
+
+ try:
+ if fs.islink(t.get_internal_path()):
+ fs.symlink(fs.readlink(t.get_internal_path()), tempfile)
+ else:
+ fs.copy2(t.get_internal_path(), tempfile)
+ fs.rename(tempfile, cachefile)
+ 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
+ # same time we did, or else that there was some problem like
+ # the CacheDir being on a separate file system that's full.
+ # In any case, inability to push a file to cache doesn't affect
+ # the correctness of the build, so just print a warning.
+ msg = errfmt % (str(target), cachefile)
+ SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning, msg)
+
+CachePush = SCons.Action.Action(CachePushFunc, None)
+
+
+class CacheDir(object):
+
+ def __init__(self, path):
+ """
+ Initialize a CacheDir object.
+
+ The cache configuration is stored in the object. It
+ is read from the config file in the supplied path if
+ one exists, if not the config file is created and
+ the default config is written, as well as saved in the object.
+ """
+ self.requests = 0
+ self.hits = 0
+ self.path = path
+ self.current_cache_debug = None
+ self.debugFP = None
+ self.config = dict()
+ if path is None:
+ return
+
+ self._readconfig(path)
+
+
+ def _readconfig(self, path):
+ """
+ Read the cache config.
+
+ If directory or config file do not exist, create. Take advantage
+ of Py3 capability in os.makedirs() and in file open(): just try
+ the operation and handle failure appropriately.
+
+ Omit the check for old cache format, assume that's old enough
+ there will be none of those left to worry about.
+
+ :param path: path to the cache directory
+ """
+ config_file = os.path.join(path, 'config')
+ try:
+ os.makedirs(path, exist_ok=True)
+ except FileExistsError:
+ pass
+ except OSError:
+ msg = "Failed to create cache directory " + path
+ raise SCons.Errors.SConsEnvironmentError(msg)
+
+ try:
+ with open(config_file, 'x') as config:
+ self.config['prefix_len'] = 2
+ try:
+ json.dump(self.config, config)
+ except Exception:
+ msg = "Failed to write cache configuration for " + path
+ raise SCons.Errors.SConsEnvironmentError(msg)
+ except FileExistsError:
+ try:
+ with open(config_file) as config:
+ self.config = json.load(config)
+ except ValueError:
+ msg = "Failed to read cache configuration for " + path
+ raise SCons.Errors.SConsEnvironmentError(msg)
+
+
+ def CacheDebug(self, fmt, target, cachefile):
+ if cache_debug != self.current_cache_debug:
+ if cache_debug == '-':
+ self.debugFP = sys.stdout
+ elif cache_debug:
+ self.debugFP = open(cache_debug, 'w')
+ else:
+ self.debugFP = None
+ self.current_cache_debug = cache_debug
+ if self.debugFP:
+ self.debugFP.write(fmt % (target, os.path.split(cachefile)[1]))
+ self.debugFP.write("requests: %d, hits: %d, misses: %d, hit rate: %.2f%%\n" %
+ (self.requests, self.hits, self.misses, self.hit_ratio))
+
+ @property
+ def hit_ratio(self):
+ return (100.0 * self.hits / self.requests if self.requests > 0 else 100)
+
+ @property
+ def misses(self):
+ return self.requests - self.hits
+
+ def is_enabled(self):
+ return cache_enabled and self.path is not None
+
+ def is_readonly(self):
+ return cache_readonly
+
+ def cachepath(self, node):
+ """
+ """
+ if not self.is_enabled():
+ return None, None
+
+ sig = node.get_cachedir_bsig()
+
+ subdir = sig[:self.config['prefix_len']].upper()
+
+ dir = os.path.join(self.path, subdir)
+ return dir, os.path.join(dir, sig)
+
+ def retrieve(self, node):
+ """
+ This method is called from multiple threads in a parallel build,
+ so only do thread safe stuff here. Do thread unsafe stuff in
+ built().
+
+ Note that there's a special trick here with the execute flag
+ (one that's not normally done for other actions). Basically
+ if the user requested a no_exec (-n) build, then
+ SCons.Action.execute_actions is set to 0 and when any action
+ is called, it does its showing but then just returns zero
+ instead of actually calling the action execution operation.
+ The problem for caching is that if the file does NOT exist in
+ cache then the CacheRetrieveString won't return anything to
+ show for the task, but the Action.__call__ won't call
+ CacheRetrieveFunc; instead it just returns zero, which makes
+ the code below think that the file *was* successfully
+ retrieved from the cache, therefore it doesn't do any
+ subsequent building. However, the CacheRetrieveString didn't
+ print anything because it didn't actually exist in the cache,
+ and no more build actions will be performed, so the user just
+ sees nothing. The fix is to tell Action.__call__ to always
+ execute the CacheRetrieveFunc and then have the latter
+ explicitly check SCons.Action.execute_actions itself.
+ """
+ if not self.is_enabled():
+ return False
+
+ env = node.get_build_env()
+ if cache_show:
+ if CacheRetrieveSilent(node, [], env, execute=1) == 0:
+ node.build(presub=0, execute=0)
+ return True
+ else:
+ if CacheRetrieve(node, [], env, execute=1) == 0:
+ return True
+
+ return False
+
+ def push(self, node):
+ if self.is_readonly() or not self.is_enabled():
+ return
+ return CachePush(node, [], node.get_build_env())
+
+ def push_if_forced(self, node):
+ if cache_force:
+ return self.push(node)
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: