Package SCons :: Module CacheDir
[hide private]
[frames] | no frames]

Source Code for Module SCons.CacheDir

  1  # 
  2  # Copyright (c) 2001 - 2019 The SCons Foundation 
  3  # 
  4  # Permission is hereby granted, free of charge, to any person obtaining 
  5  # a copy of this software and associated documentation files (the 
  6  # "Software"), to deal in the Software without restriction, including 
  7  # without limitation the rights to use, copy, modify, merge, publish, 
  8  # distribute, sublicense, and/or sell copies of the Software, and to 
  9  # permit persons to whom the Software is furnished to do so, subject to 
 10  # the following conditions: 
 11  # 
 12  # The above copyright notice and this permission notice shall be included 
 13  # in all copies or substantial portions of the Software. 
 14  # 
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 
 16  # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 
 17  # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 18  # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
 19  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
 20  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
 21  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
 22  # 
 23   
 24  __revision__ = "src/engine/SCons/CacheDir.py e724ae812eb96f4858a132f5b8c769724744faf6 2019-07-21 00:04:47 bdeegan" 
 25   
 26  __doc__ = """ 
 27  CacheDir support 
 28  """ 
 29   
 30  import hashlib 
 31  import json 
 32  import os 
 33  import stat 
 34  import sys 
 35   
 36  import SCons.Action 
 37  import SCons.Warnings 
 38  from SCons.Util import PY3 
 39   
 40  cache_enabled = True 
 41  cache_debug = False 
 42  cache_force = False 
 43  cache_show = False 
 44  cache_readonly = False 
45 46 -def CacheRetrieveFunc(target, source, env):
47 t = target[0] 48 fs = t.fs 49 cd = env.get_CacheDir() 50 cd.requests += 1 51 cachedir, cachefile = cd.cachepath(t) 52 if not fs.exists(cachefile): 53 cd.CacheDebug('CacheRetrieve(%s): %s not in cache\n', t, cachefile) 54 return 1 55 cd.hits += 1 56 cd.CacheDebug('CacheRetrieve(%s): retrieving from %s\n', t, cachefile) 57 if SCons.Action.execute_actions: 58 if fs.islink(cachefile): 59 fs.symlink(fs.readlink(cachefile), t.get_internal_path()) 60 else: 61 env.copy_from_cache(cachefile, t.get_internal_path()) 62 try: 63 os.utime(cachefile, None) 64 except OSError: 65 pass 66 st = fs.stat(cachefile) 67 fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) 68 return 0
69
70 -def CacheRetrieveString(target, source, env):
71 t = target[0] 72 fs = t.fs 73 cd = env.get_CacheDir() 74 cachedir, cachefile = cd.cachepath(t) 75 if t.fs.exists(cachefile): 76 return "Retrieved `%s' from cache" % t.get_internal_path() 77 return None
78 79 CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString) 80 81 CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
82 83 -def CachePushFunc(target, source, env):
84 if cache_readonly: 85 return 86 87 t = target[0] 88 if t.nocache: 89 return 90 fs = t.fs 91 cd = env.get_CacheDir() 92 cachedir, cachefile = cd.cachepath(t) 93 if fs.exists(cachefile): 94 # Don't bother copying it if it's already there. Note that 95 # usually this "shouldn't happen" because if the file already 96 # existed in cache, we'd have retrieved the file from there, 97 # not built it. This can happen, though, in a race, if some 98 # other person running the same build pushes their copy to 99 # the cache after we decide we need to build it but before our 100 # build completes. 101 cd.CacheDebug('CachePush(%s): %s already exists in cache\n', t, cachefile) 102 return 103 104 cd.CacheDebug('CachePush(%s): pushing to %s\n', t, cachefile) 105 106 tempfile = cachefile+'.tmp'+str(os.getpid()) 107 errfmt = "Unable to copy %s to cache. Cache file is %s" 108 109 if not fs.isdir(cachedir): 110 try: 111 fs.makedirs(cachedir) 112 except EnvironmentError: 113 # We may have received an exception because another process 114 # has beaten us creating the directory. 115 if not fs.isdir(cachedir): 116 msg = errfmt % (str(target), cachefile) 117 raise SCons.Errors.SConsEnvironmentError(msg) 118 119 try: 120 if fs.islink(t.get_internal_path()): 121 fs.symlink(fs.readlink(t.get_internal_path()), tempfile) 122 else: 123 fs.copy2(t.get_internal_path(), tempfile) 124 fs.rename(tempfile, cachefile) 125 st = fs.stat(t.get_internal_path()) 126 fs.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) 127 except EnvironmentError: 128 # It's possible someone else tried writing the file at the 129 # same time we did, or else that there was some problem like 130 # the CacheDir being on a separate file system that's full. 131 # In any case, inability to push a file to cache doesn't affect 132 # the correctness of the build, so just print a warning. 133 msg = errfmt % (str(target), cachefile) 134 SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning, msg)
135 136 CachePush = SCons.Action.Action(CachePushFunc, None) 137 138 # Nasty hack to cut down to one warning for each cachedir path that needs 139 # upgrading. 140 warned = dict()
141 142 -class CacheDir(object):
143
144 - def __init__(self, path):
145 """ 146 Initialize a CacheDir object. 147 148 The cache configuration is stored in the object. It 149 is read from the config file in the supplied path if 150 one exists, if not the config file is created and 151 the default config is written, as well as saved in the object. 152 """ 153 self.requests = 0 154 self.hits = 0 155 self.path = path 156 self.current_cache_debug = None 157 self.debugFP = None 158 self.config = dict() 159 if path is None: 160 return 161 162 if PY3: 163 self._readconfig3(path) 164 else: 165 self._readconfig2(path)
166 167
168 - def _readconfig3(self, path):
169 """ 170 Python3 version of reading the cache config. 171 172 If directory or config file do not exist, create. Take advantage 173 of Py3 capability in os.makedirs() and in file open(): just try 174 the operation and handle failure appropriately. 175 176 Omit the check for old cache format, assume that's old enough 177 there will be none of those left to worry about. 178 179 :param path: path to the cache directory 180 """ 181 config_file = os.path.join(path, 'config') 182 try: 183 os.makedirs(path, exist_ok=True) 184 except FileExistsError: 185 pass 186 except OSError: 187 msg = "Failed to create cache directory " + path 188 raise SCons.Errors.EnvironmentError(msg) 189 190 try: 191 with open(config_file, 'x') as config: 192 self.config['prefix_len'] = 2 193 try: 194 json.dump(self.config, config) 195 except Exception: 196 msg = "Failed to write cache configuration for " + path 197 raise SCons.Errors.EnvironmentError(msg) 198 except FileExistsError: 199 try: 200 with open(config_file) as config: 201 self.config = json.load(config) 202 except ValueError: 203 msg = "Failed to read cache configuration for " + path 204 raise SCons.Errors.EnvironmentError(msg)
205 206
207 - def _readconfig2(self, path):
208 """ 209 Python2 version of reading cache config. 210 211 See if there is a config file in the cache directory. If there is, 212 use it. If there isn't, and the directory exists and isn't empty, 213 produce a warning. If the directory does not exist or is empty, 214 write a config file. 215 216 :param path: path to the cache directory 217 """ 218 config_file = os.path.join(path, 'config') 219 if not os.path.exists(config_file): 220 # A note: There is a race hazard here if two processes start and 221 # attempt to create the cache directory at the same time. However, 222 # Python 2.x does not give you the option to do exclusive file 223 # creation (not even the option to error on opening an existing 224 # file for writing...). The ordering of events here is an attempt 225 # to alleviate this, on the basis that it's a pretty unlikely 226 # occurrence (would require two builds with a brand new cache 227 # directory) 228 if os.path.isdir(path) and any(f != "config" for f in os.listdir(path)): 229 self.config['prefix_len'] = 1 230 # When building the project I was testing this on, the warning 231 # was output over 20 times. That seems excessive 232 global warned 233 if self.path not in warned: 234 msg = "Please upgrade your cache by running " +\ 235 "scons-configure-cache.py " + self.path 236 SCons.Warnings.warn(SCons.Warnings.CacheVersionWarning, msg) 237 warned[self.path] = True 238 else: 239 if not os.path.isdir(path): 240 try: 241 os.makedirs(path) 242 except OSError: 243 # If someone else is trying to create the directory at 244 # the same time as me, bad things will happen 245 msg = "Failed to create cache directory " + path 246 raise SCons.Errors.SConsEnvironmentError(msg) 247 248 self.config['prefix_len'] = 2 249 if not os.path.exists(config_file): 250 try: 251 with open(config_file, 'w') as config: 252 json.dump(self.config, config) 253 except Exception: 254 msg = "Failed to write cache configuration for " + path 255 raise SCons.Errors.SConsEnvironmentError(msg) 256 else: 257 try: 258 with open(config_file) as config: 259 self.config = json.load(config) 260 except ValueError: 261 msg = "Failed to read cache configuration for " + path 262 raise SCons.Errors.SConsEnvironmentError(msg)
263 264
265 - def CacheDebug(self, fmt, target, cachefile):
266 if cache_debug != self.current_cache_debug: 267 if cache_debug == '-': 268 self.debugFP = sys.stdout 269 elif cache_debug: 270 self.debugFP = open(cache_debug, 'w') 271 else: 272 self.debugFP = None 273 self.current_cache_debug = cache_debug 274 if self.debugFP: 275 self.debugFP.write(fmt % (target, os.path.split(cachefile)[1])) 276 self.debugFP.write("requests: %d, hits: %d, misses: %d, hit rate: %.2f%%\n" % 277 (self.requests, self.hits, self.misses, self.hit_ratio))
278 279 @property
280 - def hit_ratio(self):
281 return (100.0 * self.hits / self.requests if self.requests > 0 else 100)
282 283 @property
284 - def misses(self):
285 return self.requests - self.hits
286
287 - def is_enabled(self):
288 return cache_enabled and self.path is not None
289
290 - def is_readonly(self):
291 return cache_readonly
292
293 - def cachepath(self, node):
294 """ 295 """ 296 if not self.is_enabled(): 297 return None, None 298 299 sig = node.get_cachedir_bsig() 300 301 subdir = sig[:self.config['prefix_len']].upper() 302 303 dir = os.path.join(self.path, subdir) 304 return dir, os.path.join(dir, sig)
305
306 - def retrieve(self, node):
307 """ 308 This method is called from multiple threads in a parallel build, 309 so only do thread safe stuff here. Do thread unsafe stuff in 310 built(). 311 312 Note that there's a special trick here with the execute flag 313 (one that's not normally done for other actions). Basically 314 if the user requested a no_exec (-n) build, then 315 SCons.Action.execute_actions is set to 0 and when any action 316 is called, it does its showing but then just returns zero 317 instead of actually calling the action execution operation. 318 The problem for caching is that if the file does NOT exist in 319 cache then the CacheRetrieveString won't return anything to 320 show for the task, but the Action.__call__ won't call 321 CacheRetrieveFunc; instead it just returns zero, which makes 322 the code below think that the file *was* successfully 323 retrieved from the cache, therefore it doesn't do any 324 subsequent building. However, the CacheRetrieveString didn't 325 print anything because it didn't actually exist in the cache, 326 and no more build actions will be performed, so the user just 327 sees nothing. The fix is to tell Action.__call__ to always 328 execute the CacheRetrieveFunc and then have the latter 329 explicitly check SCons.Action.execute_actions itself. 330 """ 331 if not self.is_enabled(): 332 return False 333 334 env = node.get_build_env() 335 if cache_show: 336 if CacheRetrieveSilent(node, [], env, execute=1) == 0: 337 node.build(presub=0, execute=0) 338 return True 339 else: 340 if CacheRetrieve(node, [], env, execute=1) == 0: 341 return True 342 343 return False
344
345 - def push(self, node):
346 if self.is_readonly() or not self.is_enabled(): 347 return 348 return CachePush(node, [], node.get_build_env())
349
350 - def push_if_forced(self, node):
351 if cache_force: 352 return self.push(node)
353 354 # Local Variables: 355 # tab-width:4 356 # indent-tabs-mode:nil 357 # End: 358 # vim: set expandtab tabstop=4 shiftwidth=4: 359