2016-07-28 21:09:00 +02:00
|
|
|
#!/USSR/bin/python
|
|
|
|
# encoding: utf-8
|
|
|
|
"""
|
2018-03-27 20:30:46 +02:00
|
|
|
Prune the scons cache.
|
|
|
|
|
2016-07-28 21:09:00 +02:00
|
|
|
This script, borrowed from some waf code, with a stand alone interface, provides a way to
|
|
|
|
remove files from the cache on an LRU (least recently used) basis to prevent the scons cache
|
|
|
|
from outgrowing the storage capacity.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Inspired by: https://github.com/krig/waf/blob/master/waflib/extras/lru_cache.py
|
|
|
|
# Thomas Nagy 2011
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import collections
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER = logging.getLogger("scons.cache.prune.lru") # type: ignore
|
2016-07-28 21:09:00 +02:00
|
|
|
|
2018-03-26 17:25:04 +02:00
|
|
|
GIGBYTES = 1024 * 1024 * 1024
|
2016-07-28 21:09:00 +02:00
|
|
|
|
2018-03-27 20:30:46 +02:00
|
|
|
CacheItem = collections.namedtuple("CacheContents", ["path", "time", "size"])
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
|
2022-01-19 23:32:33 +01:00
|
|
|
def get_cachefile_size(file_path, is_cksum):
|
2021-04-09 22:54:20 +02:00
|
|
|
"""Get the size of the cachefile."""
|
2022-01-19 23:32:33 +01:00
|
|
|
if is_cksum:
|
2021-04-09 22:54:20 +02:00
|
|
|
size = 0
|
|
|
|
for cksum_path in os.listdir(file_path):
|
|
|
|
cksum_path = os.path.join(file_path, cksum_path)
|
|
|
|
size += os.stat(cksum_path).st_size
|
|
|
|
else:
|
|
|
|
size = os.stat(file_path).st_size
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
2016-07-28 21:09:00 +02:00
|
|
|
def collect_cache_contents(cache_path):
|
2018-03-27 20:30:46 +02:00
|
|
|
"""Collect the cache contents."""
|
2016-07-28 21:09:00 +02:00
|
|
|
# map folder names to timestamps
|
|
|
|
contents = []
|
|
|
|
total = 0
|
|
|
|
|
|
|
|
# collect names of directories and creation times
|
|
|
|
for name in os.listdir(cache_path):
|
|
|
|
path = os.path.join(cache_path, name)
|
|
|
|
|
|
|
|
if os.path.isdir(path):
|
|
|
|
for file_name in os.listdir(path):
|
|
|
|
file_path = os.path.join(path, file_name)
|
2021-04-09 22:54:20 +02:00
|
|
|
# Cache prune script is allowing only directories with this extension
|
2022-01-19 23:32:33 +01:00
|
|
|
# which comes from the validate_cache_dir.py tool in SCons, it must match
|
2021-04-09 22:54:20 +02:00
|
|
|
# the extension set in that file.
|
2022-01-19 23:32:33 +01:00
|
|
|
cksum_type = False
|
|
|
|
if os.path.isdir(file_path):
|
|
|
|
hash_length = -32
|
2024-05-17 00:00:17 +02:00
|
|
|
tmp_length = -len(".cksum.tmp") + hash_length
|
|
|
|
cksum_type = (
|
|
|
|
file_path.lower().endswith(".cksum")
|
|
|
|
or file_path.lower().endswith(".del")
|
|
|
|
or file_path.lower()[tmp_length:hash_length] == ".cksum.tmp"
|
|
|
|
)
|
2022-01-19 23:32:33 +01:00
|
|
|
|
|
|
|
if not cksum_type:
|
|
|
|
LOGGER.warning(
|
|
|
|
"cache item %s is a directory and not a file. "
|
2024-05-17 00:00:17 +02:00
|
|
|
"The cache may be corrupt.",
|
|
|
|
file_path,
|
|
|
|
)
|
2022-01-19 23:32:33 +01:00
|
|
|
continue
|
2016-07-28 21:09:00 +02:00
|
|
|
|
2018-04-25 22:44:27 +02:00
|
|
|
try:
|
2024-05-17 00:00:17 +02:00
|
|
|
item = CacheItem(
|
|
|
|
path=file_path,
|
|
|
|
time=os.stat(file_path).st_atime,
|
|
|
|
size=get_cachefile_size(file_path, cksum_type),
|
|
|
|
)
|
2018-04-25 22:44:27 +02:00
|
|
|
|
|
|
|
total += item.size
|
2016-07-28 21:09:00 +02:00
|
|
|
|
2018-04-25 22:44:27 +02:00
|
|
|
contents.append(item)
|
|
|
|
except OSError as err:
|
|
|
|
LOGGER.warning("Ignoring error querying file %s : %s", file_path, err)
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
return (total, contents)
|
|
|
|
|
|
|
|
|
|
|
|
def prune_cache(cache_path, cache_size_gb, clean_ratio):
|
2018-03-27 20:30:46 +02:00
|
|
|
"""Prune the cache."""
|
2016-07-28 21:09:00 +02:00
|
|
|
# This function is taken as is from waf, with the interface cleaned up and some minor
|
|
|
|
# stylistic changes.
|
|
|
|
|
|
|
|
cache_size = cache_size_gb * GIGBYTES
|
|
|
|
|
|
|
|
(total_size, contents) = collect_cache_contents(cache_path)
|
|
|
|
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.info("cache size %d, quota %d", total_size, cache_size)
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
if total_size >= cache_size:
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.info("trimming the cache since %d > %d", total_size, cache_size)
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
# make a list to sort the folders' by timestamp
|
|
|
|
contents.sort(key=lambda x: x.time, reverse=True) # sort by timestamp
|
|
|
|
|
|
|
|
# now that the contents of things to delete is sorted by timestamp in reverse order, we
|
|
|
|
# just delete things until the total_size falls below the target cache size ratio.
|
|
|
|
while total_size >= cache_size * clean_ratio:
|
2018-03-27 20:30:46 +02:00
|
|
|
if not contents:
|
2024-05-17 00:00:17 +02:00
|
|
|
LOGGER.error(
|
|
|
|
"cache size is over quota, and there are no files in " "the queue to delete."
|
|
|
|
)
|
2016-07-28 21:09:00 +02:00
|
|
|
return False
|
|
|
|
|
|
|
|
cache_item = contents.pop()
|
2022-01-14 23:31:22 +01:00
|
|
|
|
|
|
|
# check the atime again just to make sure something wasn't accessed while
|
|
|
|
# we pruning other files.
|
2022-12-20 23:42:20 +01:00
|
|
|
try:
|
|
|
|
if cache_item.time < os.stat(cache_item.path).st_atime:
|
|
|
|
continue
|
|
|
|
except FileNotFoundError as err:
|
|
|
|
LOGGER.warning("Unable to find file %s : %s", cache_item, err)
|
2022-01-14 23:31:22 +01:00
|
|
|
continue
|
|
|
|
|
2016-07-28 21:09:00 +02:00
|
|
|
to_remove = cache_item.path + ".del"
|
|
|
|
try:
|
|
|
|
os.rename(cache_item.path, to_remove)
|
2018-04-25 22:44:27 +02:00
|
|
|
except Exception as err: # pylint: disable=broad-except
|
2016-07-28 21:09:00 +02:00
|
|
|
# another process may have already cleared the file.
|
2018-04-25 22:44:27 +02:00
|
|
|
LOGGER.warning("Unable to rename %s : %s", cache_item, err)
|
2016-07-28 21:09:00 +02:00
|
|
|
else:
|
|
|
|
try:
|
2021-04-09 22:54:20 +02:00
|
|
|
if os.path.isdir(to_remove):
|
|
|
|
shutil.rmtree(to_remove)
|
|
|
|
else:
|
|
|
|
os.remove(to_remove)
|
2016-07-28 21:09:00 +02:00
|
|
|
total_size -= cache_item.size
|
2018-03-27 20:30:46 +02:00
|
|
|
except Exception as err: # pylint: disable=broad-except
|
2016-07-28 21:09:00 +02:00
|
|
|
# this should not happen, but who knows?
|
2024-05-17 00:00:17 +02:00
|
|
|
LOGGER.error(
|
|
|
|
"error [%s, %s] removing file '%s', " "please report this error",
|
|
|
|
err,
|
|
|
|
type(err),
|
|
|
|
to_remove,
|
|
|
|
)
|
2016-07-28 21:09:00 +02:00
|
|
|
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.info("total cache size at the end of pruning: %d", total_size)
|
2016-07-28 21:09:00 +02:00
|
|
|
return True
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.info("cache size (%d) is currently within boundaries", total_size)
|
|
|
|
return True
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2018-03-27 20:30:46 +02:00
|
|
|
"""Execute Main entry."""
|
2018-12-13 18:03:09 +01:00
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
2016-07-28 21:09:00 +02:00
|
|
|
parser = argparse.ArgumentParser(description="SCons cache pruning tool")
|
|
|
|
|
2018-03-26 17:25:04 +02:00
|
|
|
parser.add_argument("--cache-dir", "-d", default=None, help="path to the cache directory.")
|
2019-02-19 16:50:57 +01:00
|
|
|
parser.add_argument(
|
2024-05-17 00:00:17 +02:00
|
|
|
"--cache-size", "-s", default=200, type=int, help="maximum size of cache in GB."
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--prune-ratio",
|
|
|
|
"-p",
|
|
|
|
default=0.8,
|
|
|
|
type=float,
|
|
|
|
help=(
|
|
|
|
"ratio (as 1.0 > x > 0) of total cache size to prune " "to when cache exceeds quota."
|
|
|
|
),
|
|
|
|
)
|
2016-07-28 21:09:00 +02:00
|
|
|
parser.add_argument("--print-cache-dir", default=False, action="store_true")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
if args.cache_dir is None or not os.path.isdir(args.cache_dir):
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.error("must specify a valid cache path, [%s]", args.cache_dir)
|
2016-07-28 21:09:00 +02:00
|
|
|
exit(1)
|
|
|
|
|
2024-05-17 00:00:17 +02:00
|
|
|
ok = prune_cache(
|
|
|
|
cache_path=args.cache_dir, cache_size_gb=args.cache_size, clean_ratio=args.prune_ratio
|
|
|
|
)
|
2016-07-28 21:09:00 +02:00
|
|
|
|
|
|
|
if not ok:
|
2018-03-27 20:30:46 +02:00
|
|
|
LOGGER.error("encountered error cleaning the cache. exiting.")
|
2016-07-28 21:09:00 +02:00
|
|
|
exit(1)
|
|
|
|
|
2018-03-26 17:25:04 +02:00
|
|
|
|
2016-07-28 21:09:00 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|