3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40 CACHE_PREFIX_LEN
= 2 # first two characters used as subdirectory name
42 b
"Signature: 8a477f597d28d172789f06886806bc55\n"
43 b
"# SCons cache directory - see https://bford.info/cachedir/\n"
50 cache_readonly
= False
51 cache_tmp_uuid
= uuid
.uuid4().hex
53 def CacheRetrieveFunc(target
, source
, env
) -> int:
56 cd
= env
.get_CacheDir()
58 cachedir
, cachefile
= cd
.cachepath(t
)
59 if not fs
.exists(cachefile
):
60 cd
.CacheDebug('CacheRetrieve(%s): %s not in cache\n', t
, cachefile
)
63 cd
.CacheDebug('CacheRetrieve(%s): retrieving from %s\n', t
, cachefile
)
64 if SCons
.Action
.execute_actions
:
65 if fs
.islink(cachefile
):
66 fs
.symlink(fs
.readlink(cachefile
), t
.get_internal_path())
68 cd
.copy_from_cache(env
, cachefile
, t
.get_internal_path())
70 os
.utime(cachefile
, None)
73 st
= fs
.stat(cachefile
)
74 fs
.chmod(t
.get_internal_path(), stat
.S_IMODE(st
[stat
.ST_MODE
]) | stat
.S_IWRITE
)
77 def CacheRetrieveString(target
, source
, env
) -> str:
80 cd
= env
.get_CacheDir()
81 cachedir
, cachefile
= cd
.cachepath(t
)
82 if t
.fs
.exists(cachefile
):
83 return "Retrieved `%s' from cache" % t
.get_internal_path()
86 CacheRetrieve
= SCons
.Action
.Action(CacheRetrieveFunc
, CacheRetrieveString
)
88 CacheRetrieveSilent
= SCons
.Action
.Action(CacheRetrieveFunc
, None)
90 def CachePushFunc(target
, source
, env
) -> None:
98 cd
= env
.get_CacheDir()
99 cachedir
, cachefile
= cd
.cachepath(t
)
100 if fs
.exists(cachefile
):
101 # Don't bother copying it if it's already there. Note that
102 # usually this "shouldn't happen" because if the file already
103 # existed in cache, we'd have retrieved the file from there,
104 # not built it. This can happen, though, in a race, if some
105 # other person running the same build pushes their copy to
106 # the cache after we decide we need to build it but before our
108 cd
.CacheDebug('CachePush(%s): %s already exists in cache\n', t
, cachefile
)
111 cd
.CacheDebug('CachePush(%s): pushing to %s\n', t
, cachefile
)
113 tempfile
= "%s.tmp%s"%(cachefile
,cache_tmp_uuid
)
114 errfmt
= "Unable to copy %s to cache. Cache file is %s"
117 fs
.makedirs(cachedir
, exist_ok
=True)
119 msg
= errfmt
% (str(target
), cachefile
)
120 raise SCons
.Errors
.SConsEnvironmentError(msg
)
122 if fs
.islink(t
.get_internal_path()):
123 fs
.symlink(fs
.readlink(t
.get_internal_path()), tempfile
)
125 cd
.copy_to_cache(env
, t
.get_internal_path(), tempfile
)
126 fs
.rename(tempfile
, cachefile
)
129 # It's possible someone else tried writing the file at the
130 # same time we did, or else that there was some problem like
131 # the CacheDir being on a separate file system that's full.
132 # In any case, inability to push a file to cache doesn't affect
133 # the correctness of the build, so just print a warning.
134 msg
= errfmt
% (str(t
), cachefile
)
135 cd
.CacheDebug(errfmt
+ '\n', str(t
), cachefile
)
136 SCons
.Warnings
.warn(SCons
.Warnings
.CacheWriteErrorWarning
, msg
)
138 CachePush
= SCons
.Action
.Action(CachePushFunc
, None)
143 def __init__(self
, path
) -> None:
144 """Initialize a CacheDir object.
146 The cache configuration is stored in the object. It
147 is read from the config file in the supplied path if
148 one exists, if not the config file is created and
149 the default config is written, as well as saved in the object.
154 self
.current_cache_debug
= None
158 self
._readconfig
(path
)
160 def _add_config(self
, path
: str) -> None:
161 """Create the cache config file in *path*.
163 Locking isn't necessary in the normal case - when the cachedir is
164 being created - because it's written to a unique directory first,
165 before the directory is renamed. But it is legal to call CacheDir
166 with an existing directory, which may be missing the config file,
167 and in that case we do need locking. Simpler to always lock.
169 config_file
= os
.path
.join(path
, 'config')
170 # TODO: this breaks the "unserializable config object" test which
171 # does some crazy stuff, so for now don't use setdefault. It does
172 # seem like it would be better to preserve an exisiting value.
173 # self.config.setdefault('prefix_len', CACHE_PREFIX_LEN)
174 self
.config
['prefix_len'] = CACHE_PREFIX_LEN
175 with SCons
.Util
.FileLock(config_file
, timeout
=5, writer
=True), open(
179 json
.dump(self
.config
, config
)
181 msg
= "Failed to write cache configuration for " + path
182 raise SCons
.Errors
.SConsEnvironmentError(msg
)
184 # Add the tag file "carelessly" - the contents are not used by SCons
185 # so we don't care about the chance of concurrent writes.
187 tagfile
= os
.path
.join(path
, "CACHEDIR.TAG")
188 with
open(tagfile
, 'xb') as cachedir_tag
:
189 cachedir_tag
.write(CACHE_TAG
)
190 except FileExistsError
:
193 def _mkdir_atomic(self
, path
: str) -> bool:
194 """Create cache directory at *path*.
196 Uses directory renaming to avoid races. If we are actually
197 creating the dir, populate it with the metadata files at the
198 same time as that's the safest way. But it's not illegal to point
199 CacheDir at an existing directory that wasn't a cache previously,
200 so we may have to do that elsewhere, too.
203 ``True`` if it we created the dir, ``False`` if already existed,
206 SConsEnvironmentError: if we tried and failed to create the cache.
208 directory
= os
.path
.abspath(path
)
209 if os
.path
.exists(directory
):
213 tempdir
= tempfile
.TemporaryDirectory(dir=os
.path
.dirname(directory
))
215 msg
= "Failed to create cache directory " + path
216 raise SCons
.Errors
.SConsEnvironmentError(msg
) from e
217 self
._add
_config
(tempdir
.name
)
220 os
.rename(tempdir
.name
, directory
)
222 except Exception as e
:
223 # did someone else get there first?
224 if os
.path
.isdir(directory
):
226 msg
= "Failed to create cache directory " + path
227 raise SCons
.Errors
.SConsEnvironmentError(msg
) from e
229 def _readconfig(self
, path
: str) -> None:
230 """Read the cache config from *path*.
232 If directory or config file do not exist, create and populate.
234 config_file
= os
.path
.join(path
, 'config')
235 created
= self
._mkdir
_atomic
(path
)
236 if not created
and not os
.path
.isfile(config_file
):
237 # Could have been passed an empty directory
238 self
._add
_config
(path
)
240 with SCons
.Util
.FileLock(config_file
, timeout
=5, writer
=False), open(
243 self
.config
= json
.load(config
)
244 except (ValueError, json
.decoder
.JSONDecodeError
):
245 msg
= "Failed to read cache configuration for " + path
246 raise SCons
.Errors
.SConsEnvironmentError(msg
)
248 def CacheDebug(self
, fmt
, target
, cachefile
) -> None:
249 if cache_debug
!= self
.current_cache_debug
:
250 if cache_debug
== '-':
251 self
.debugFP
= sys
.stdout
253 def debug_cleanup(debugFP
) -> None:
256 self
.debugFP
= open(cache_debug
, 'w')
257 atexit
.register(debug_cleanup
, self
.debugFP
)
260 self
.current_cache_debug
= cache_debug
262 self
.debugFP
.write(fmt
% (target
, os
.path
.split(cachefile
)[1]))
263 self
.debugFP
.write("requests: %d, hits: %d, misses: %d, hit rate: %.2f%%\n" %
264 (self
.requests
, self
.hits
, self
.misses
, self
.hit_ratio
))
267 def copy_from_cache(cls
, env
, src
, dst
) -> str:
268 """Copy a file from cache."""
269 if env
.cache_timestamp_newer
:
270 return env
.fs
.copy(src
, dst
)
272 return env
.fs
.copy2(src
, dst
)
275 def copy_to_cache(cls
, env
, src
, dst
) -> str:
276 """Copy a file to cache.
278 Just use the FS copy2 ("with metadata") method, except do an additional
279 check and if necessary a chmod to ensure the cachefile is writeable,
280 to forestall permission problems if the cache entry is later updated.
283 result
= env
.fs
.copy2(src
, dst
)
284 st
= stat
.S_IMODE(os
.stat(result
).st_mode
)
285 if not st | stat
.S_IWRITE
:
286 os
.chmod(dst
, st | stat
.S_IWRITE
)
288 except AttributeError as ex
:
289 raise OSError from ex
292 def hit_ratio(self
) -> float:
293 return (100.0 * self
.hits
/ self
.requests
if self
.requests
> 0 else 100)
296 def misses(self
) -> int:
297 return self
.requests
- self
.hits
299 def is_enabled(self
) -> bool:
300 return cache_enabled
and self
.path
is not None
302 def is_readonly(self
) -> bool:
303 return cache_readonly
305 def get_cachedir_csig(self
, node
) -> str:
306 cachedir
, cachefile
= self
.cachepath(node
)
307 if cachefile
and os
.path
.exists(cachefile
):
308 return SCons
.Util
.hash_file_signature(cachefile
, SCons
.Node
.FS
.File
.hash_chunksize
)
310 def cachepath(self
, node
) -> tuple:
311 """Return where to cache a file.
313 Given a Node, obtain the configured cache directory and
314 the path to the cached file, which is generated from the
315 node's build signature. If caching is not enabled for the
316 None, return a tuple of None.
318 if not self
.is_enabled():
321 sig
= node
.get_cachedir_bsig()
322 subdir
= sig
[:self
.config
['prefix_len']].upper()
323 cachedir
= os
.path
.join(self
.path
, subdir
)
324 return cachedir
, os
.path
.join(cachedir
, sig
)
326 def retrieve(self
, node
) -> bool:
327 """Retrieve a node from cache.
329 Returns True if a successful retrieval resulted.
331 This method is called from multiple threads in a parallel build,
332 so only do thread safe stuff here. Do thread unsafe stuff in
335 Note that there's a special trick here with the execute flag
336 (one that's not normally done for other actions). Basically
337 if the user requested a no_exec (-n) build, then
338 SCons.Action.execute_actions is set to 0 and when any action
339 is called, it does its showing but then just returns zero
340 instead of actually calling the action execution operation.
341 The problem for caching is that if the file does NOT exist in
342 cache then the CacheRetrieveString won't return anything to
343 show for the task, but the Action.__call__ won't call
344 CacheRetrieveFunc; instead it just returns zero, which makes
345 the code below think that the file *was* successfully
346 retrieved from the cache, therefore it doesn't do any
347 subsequent building. However, the CacheRetrieveString didn't
348 print anything because it didn't actually exist in the cache,
349 and no more build actions will be performed, so the user just
350 sees nothing. The fix is to tell Action.__call__ to always
351 execute the CacheRetrieveFunc and then have the latter
352 explicitly check SCons.Action.execute_actions itself.
354 if not self
.is_enabled():
357 env
= node
.get_build_env()
359 if CacheRetrieveSilent(node
, [], env
, execute
=1) == 0:
360 node
.build(presub
=0, execute
=0)
363 if CacheRetrieve(node
, [], env
, execute
=1) == 0:
368 def push(self
, node
):
369 if self
.is_readonly() or not self
.is_enabled():
371 return CachePush(node
, [], node
.get_build_env())
373 def push_if_forced(self
, node
):
375 return self
.push(node
)
379 # indent-tabs-mode:nil
381 # vim: set expandtab tabstop=4 shiftwidth=4: