Skip to content

Commit 9dcf8f9

Browse files
committed
SCons: Integrate cache_limit from main repo
1 parent 27ffd8c commit 9dcf8f9

File tree

6 files changed

+153
-13
lines changed

6 files changed

+153
-13
lines changed

.github/actions/godot-cache-restore/action.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ inputs:
66
default: ${{ github.job }}
77
scons-cache:
88
description: The SCons cache path.
9-
default: ${{ github.workspace }}/.scons-cache/
9+
default: ${{ github.workspace }}/.scons_cache/
1010

1111
runs:
1212
using: composite
@@ -18,7 +18,6 @@ runs:
1818
key: ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}-${{ github.sha }}
1919

2020
restore-keys: |
21-
${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}-${{ github.sha }}
2221
${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}
2322
${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-refs/heads/${{ env.GODOT_BASE_BRANCH }}
2423
${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}

.github/actions/godot-cache-save/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ inputs:
66
default: ${{ github.job }}
77
scons-cache:
88
description: The SCons cache path.
9-
default: ${{ github.workspace }}/.scons-cache/
9+
default: ${{ github.workspace }}/.scons_cache/
1010

1111
runs:
1212
using: composite

.github/workflows/ci.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ jobs:
9393
cache-name: web-wasm32
9494

9595
env:
96-
SCONS_CACHE: ${{ github.workspace }}/.scons-cache/
9796
EM_VERSION: 3.1.39
9897

9998
steps:
@@ -116,22 +115,22 @@ jobs:
116115

117116
- name: Generate godot-cpp sources only
118117
run: |
119-
scons platform=${{ matrix.platform }} verbose=yes build_library=no ${{ matrix.flags }}
118+
scons platform=${{ matrix.platform }} verbose=yes build_library=no ${{ matrix.flags }} "cache_path=${{ github.workspace }}/.scons_cache"
120119
scons -c
121120
122121
- name: Build godot-cpp (debug)
123122
run: |
124-
scons platform=${{ matrix.platform }} verbose=yes target=template_debug ${{ matrix.flags }}
123+
scons platform=${{ matrix.platform }} verbose=yes target=template_debug ${{ matrix.flags }} "cache_path=${{ github.workspace }}/.scons_cache"
125124
126125
- name: Build test without rebuilding godot-cpp (debug)
127126
run: |
128127
cd test
129-
scons platform=${{ matrix.platform }} verbose=yes target=template_debug ${{ matrix.flags }} build_library=no
128+
scons platform=${{ matrix.platform }} verbose=yes target=template_debug ${{ matrix.flags }} "cache_path=${{ github.workspace }}/.scons_cache" build_library=no
130129
131130
- name: Build test and godot-cpp (release)
132131
run: |
133132
cd test
134-
scons platform=${{ matrix.platform }} verbose=yes target=template_release ${{ matrix.flags }}
133+
scons platform=${{ matrix.platform }} verbose=yes target=template_release ${{ matrix.flags }} "cache_path=${{ github.workspace }}/.scons_cache" cache_limit=1
135134
136135
- name: Save Godot build cache
137136
uses: ./.github/actions/godot-cache-save

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ compile_commands.json
196196
.venv
197197
venv
198198

199+
# Python modules
200+
.*_cache/
201+
199202
# Clion Configuration
200203
.idea/
201204
cmake-build*/

SConstruct

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ if unknown:
4040
for item in unknown.items():
4141
print(" " + item[0] + "=" + item[1])
4242

43-
scons_cache_path = os.environ.get("SCONS_CACHE")
44-
if scons_cache_path is not None:
45-
CacheDir(scons_cache_path)
46-
Decider("MD5")
47-
4843
cpp_tool.generate(env)
4944
library = env.GodotCPP()
5045

tools/godotcpp.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,16 @@ def options(opts, env):
337337
opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True))
338338
opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False))
339339
opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False))
340+
opts.Add(
341+
"cache_path", "Path to a directory where SCons cache files will be stored. No value disables the cache.", ""
342+
)
343+
opts.Add(
344+
(
345+
["cache_limit", "cache_limit_gb", "cache_limit_gib"],
346+
"Max size (in GiB) for the SCons cache. 0 means no limit.",
347+
"0",
348+
)
349+
)
340350

341351
# Add platform options (custom tools can override platforms)
342352
for pl in sorted(set(platforms + custom_platforms)):
@@ -390,7 +400,141 @@ def make_doc_source(target, source, env):
390400
g.close()
391401

392402

403+
def convert_size(size_bytes: int) -> str:
404+
import math
405+
406+
if size_bytes == 0:
407+
return "0 bytes"
408+
SIZE_NAMES = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
409+
index = math.floor(math.log(size_bytes, 1024))
410+
power = math.pow(1024, index)
411+
size = round(size_bytes / power, 2)
412+
return f"{size} {SIZE_NAMES[index]}"
413+
414+
415+
def get_size(start_path: str = ".") -> int:
416+
total_size = 0
417+
for dirpath, _, filenames in os.walk(start_path):
418+
for file in filenames:
419+
path = os.path.join(dirpath, file)
420+
total_size += os.path.getsize(path)
421+
return total_size
422+
423+
424+
def is_binary(path: str) -> bool:
425+
try:
426+
with open(path, encoding="utf-8") as out:
427+
out.read(1024)
428+
return False
429+
except UnicodeDecodeError:
430+
return True
431+
432+
433+
def clean_cache(cache_path: str, cache_limit: int, verbose: bool):
434+
from glob import glob
435+
436+
files = glob(os.path.join(cache_path, "*", "*"))
437+
if not files:
438+
return
439+
440+
# Remove all text files, store binary files in list of (filename, size, atime).
441+
purge = []
442+
texts = []
443+
stats = []
444+
for file in files:
445+
try:
446+
# Save file stats to rewrite after modifying.
447+
tmp_stat = os.stat(file)
448+
if is_binary(file):
449+
stats.append((file, *tmp_stat[6:8]))
450+
# Restore file stats after reading.
451+
os.utime(file, (tmp_stat[7], tmp_stat[8]))
452+
else:
453+
texts.append(file)
454+
except OSError:
455+
print(f'Failed to access cache file "{file}"; skipping.')
456+
457+
if texts:
458+
count = len(texts)
459+
for file in texts:
460+
try:
461+
os.remove(file)
462+
except OSError:
463+
print(f'Failed to remove cache file "{file}"; skipping.')
464+
count -= 1
465+
if verbose:
466+
print("Purging %d text %s from cache..." % (count, "files" if count > 1 else "file"))
467+
468+
if cache_limit:
469+
# Sort by most recent access (most sensible to keep) first. Search for the first entry where
470+
# the cache limit is reached.
471+
stats.sort(key=lambda x: x[2], reverse=True)
472+
sum = 0
473+
for index, stat in enumerate(stats):
474+
sum += stat[1]
475+
if sum > cache_limit:
476+
purge.extend([x[0] for x in stats[index:]])
477+
break
478+
479+
if purge:
480+
count = len(purge)
481+
for file in purge:
482+
try:
483+
os.remove(file)
484+
except OSError:
485+
print(f'Failed to remove cache file "{file}"; skipping.')
486+
count -= 1
487+
if verbose:
488+
print("Purging %d %s from cache..." % (count, "files" if count > 1 else "file"))
489+
490+
491+
def prepare_cache(env) -> None:
492+
import atexit
493+
494+
if env.GetOption("clean"):
495+
return
496+
497+
cache_path = None
498+
if env["cache_path"]:
499+
cache_path = env["cache_path"]
500+
elif os.environ.get("SCONS_CACHE"):
501+
print("Environment variable `SCONS_CACHE` is deprecated; use `cache_path` argument instead.")
502+
cache_path = os.environ.get("SCONS_CACHE")
503+
504+
if not cache_path:
505+
return
506+
507+
env.CacheDir(cache_path)
508+
cache_path = env.get_CacheDir().path
509+
print(f'SCons cache enabled... (path: "{cache_path}")')
510+
511+
if env["cache_limit"]:
512+
cache_limit = float(env["cache_limit"])
513+
elif os.environ.get("SCONS_CACHE_LIMIT"):
514+
print("Environment variable `SCONS_CACHE_LIMIT` is deprecated; use `cache_limit` argument instead.")
515+
cache_limit = float(os.getenv("SCONS_CACHE_LIMIT", "0")) / 1024 # Old method used MiB, convert to GiB
516+
517+
# Convert GiB to bytes; treat negative numbers as 0 (unlimited).
518+
cache_limit = max(0, int(cache_limit * 1024 * 1024 * 1024))
519+
if env["verbose"]:
520+
print(
521+
"Current cache limit is {} (used: {})".format(
522+
# FIXME: Infinity symbol `∞` breaks Windows GHA.
523+
convert_size(cache_limit) if cache_limit else "<unlimited>",
524+
convert_size(get_size(cache_path)),
525+
)
526+
)
527+
528+
atexit.register(clean_cache, cache_path, cache_limit, env["verbose"])
529+
530+
393531
def generate(env):
532+
# Setup caching logic early to catch everything.
533+
prepare_cache(env)
534+
535+
# Renamed to `content-timestamp` in SCons >= 4.2, keeping MD5 for compat.
536+
env.Decider("MD5-timestamp")
537+
394538
# Default num_jobs to local cpu count if not user specified.
395539
# SCons has a peculiarity where user-specified options won't be overridden
396540
# by SetOption, so we can rely on this to know if we should use our default.

0 commit comments

Comments
 (0)