Skip to content
This repository was archived by the owner on Feb 4, 2020. It is now read-only.

Commit ceed29f

Browse files
committed
Add support for loading config from a clcache.conf file
This is useful and the GCC version of ccache also implements a similar concept. It might be useful in situations where some default settings could be specified or when environment is not passed correctly down to the compiler chain. This change introduces support for clcache.conf file to allow clcache to read settings from there. The precedence is as follows: 1. If present, Environment variable(s) are used, or 2. [current_working_dir]\clcache.conf is loaded, or, if not found 3. [%HOME% or ~\].clcache\clcache.conf is be loaded, or if not found 4. [%ALLUSERSPROFILE% or C:\Users\].clcache\clcache.conf is loaded. In each case, once a clcache.conf file is found, no other conf file is considered and values only from this file are used. It is also loaded only once (and all its values then cached) - all to avoid unnecessary performance penalties.
1 parent 72b2b2b commit ceed29f

File tree

3 files changed

+177
-24
lines changed

3 files changed

+177
-24
lines changed

README.asciidoc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ Options
4646
Sets the maximum size of the cache in bytes.
4747
The default value is 1073741824 (1 GiB).
4848

49-
Environment Variables
50-
~~~~~~~~~~~~~~~~~~~~~
49+
Configuration
50+
~~~~~~~~~~~~~
51+
52+
Following values are read from Environment variables, and if absent, from either (in that order):
53+
- (current-working-dir)\clcache.conf
54+
- %HOME%\.clcache\clcache.conf
55+
- %ALLUSERSPROFILE%\.clcache\clcache.conf
5156

5257
CLCACHE_DIR::
5358
If set, points to the directory within which all the cached object files

clcache/__main__.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import sys
2525
import threading
2626
from tempfile import TemporaryFile
27-
from typing import Any, List, Tuple, Iterator
27+
from typing import Any, Dict, List, Tuple, Iterator
2828

2929
VERSION = "4.1.0-dev"
3030

@@ -308,6 +308,50 @@ def getIncludesContentHashForHashes(listOfHashes):
308308
return HashAlgorithm(','.join(listOfHashes).encode()).hexdigest()
309309

310310

311+
class GlobalSettings:
312+
""" Implements a common place to obain settings from. """
313+
@staticmethod
314+
def getValue(settingName, defaultValue=None):
315+
value = os.environ.get(settingName, None)
316+
if value is None: # compare to None to allow empty values
317+
value = GlobalSettings._getFromCache(settingName)
318+
return value if value is not None else defaultValue
319+
320+
# serves as a cache to only read the config file once
321+
_cache = {} # type: Dict[str, str]
322+
323+
@staticmethod
324+
def _getFromCache(settingName):
325+
if not GlobalSettings._cache:
326+
GlobalSettings._readFromFile()
327+
return GlobalSettings._cache.get(settingName, None)
328+
329+
@staticmethod
330+
def _readFromFile():
331+
GlobalSettings._cache['dummy'] = 'dummy' # so that _readFromFile is only called once
332+
333+
# prefer config in current directory
334+
filename = os.path.join(os.getcwd(), "clcache.conf")
335+
336+
# ..or in home directory..
337+
if not os.path.exists(filename):
338+
filename = os.path.join(os.path.expanduser("~"), ".clcache", "clcache.conf")
339+
340+
# or in "sysconfdir" (%ALLUSERSPROFILE%)
341+
if not os.path.exists(filename):
342+
dirname = os.environ.get('ALLUSERSPROFILE', None)
343+
filename = os.path.join(dirname if dirname else "C:\\Users", ".clcache", "clcache.conf")
344+
try:
345+
with open(filename) as f:
346+
for line in f.readlines():
347+
kv = line.split("=")
348+
if len(kv) != 2 or kv[0].startswith("#"):
349+
continue
350+
GlobalSettings._cache[kv[0].strip()] = kv[1].split("#")[0].strip()
351+
except IOError:
352+
pass # only ignore file access errors (including not-existing path)
353+
354+
311355
class CacheLock:
312356
""" Implements a lock for the object cache which
313357
can be used in 'with' statements. """
@@ -359,7 +403,7 @@ def release(self):
359403

360404
@staticmethod
361405
def forPath(path):
362-
timeoutMs = int(os.environ.get('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
406+
timeoutMs = int(GlobalSettings.getValue('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
363407
lockName = path.replace(':', '-').replace('\\', '-')
364408
return CacheLock(lockName, timeoutMs)
365409

@@ -505,10 +549,8 @@ class CacheFileStrategy:
505549
def __init__(self, cacheDirectory=None):
506550
self.dir = cacheDirectory
507551
if not self.dir:
508-
try:
509-
self.dir = os.environ["CLCACHE_DIR"]
510-
except KeyError:
511-
self.dir = os.path.join(os.path.expanduser("~"), "clcache")
552+
self.dir = GlobalSettings.getValue("CLCACHE_DIR",
553+
os.path.join(os.path.expanduser("~"), "clcache"))
512554

513555
manifestsRootDir = os.path.join(self.dir, "manifests")
514556
ensureDirectoryExists(manifestsRootDir)
@@ -593,9 +635,10 @@ def clean(self, stats, maximumSize):
593635

594636
class Cache:
595637
def __init__(self, cacheDirectory=None):
596-
if os.environ.get("CLCACHE_MEMCACHED"):
638+
memcached = GlobalSettings.getValue("CLCACHE_MEMCACHED")
639+
if memcached and memcached not in ['0', 'false', 'False']:
597640
from .storage import CacheFileWithMemcacheFallbackStrategy
598-
self.strategy = CacheFileWithMemcacheFallbackStrategy(os.environ.get("CLCACHE_MEMCACHED"),
641+
self.strategy = CacheFileWithMemcacheFallbackStrategy(memcached,
599642
cacheDirectory=cacheDirectory)
600643
else:
601644
self.strategy = CacheFileStrategy(cacheDirectory=cacheDirectory)
@@ -900,7 +943,8 @@ def getCompilerHash(compilerBinary):
900943

901944

902945
def getFileHashes(filePaths):
903-
if 'CLCACHE_SERVER' in os.environ:
946+
server = GlobalSettings.getValue('CLCACHE_SERVER')
947+
if server and server not in ['0', 'false', 'False']:
904948
pipeName = r'\\.\pipe\clcache_srv'
905949
while True:
906950
try:
@@ -939,7 +983,7 @@ def getStringHash(dataString):
939983

940984

941985
def expandBasedirPlaceholder(path):
942-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
986+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
943987
if path.startswith(BASEDIR_REPLACEMENT):
944988
if not baseDir:
945989
raise LogicException('No CLCACHE_BASEDIR set, but found relative path ' + path)
@@ -949,7 +993,7 @@ def expandBasedirPlaceholder(path):
949993

950994

951995
def collapseBasedirToPlaceholder(path):
952-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
996+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
953997
if baseDir is None:
954998
return path
955999
else:
@@ -971,8 +1015,8 @@ def ensureDirectoryExists(path):
9711015

9721016
def copyOrLink(srcFilePath, dstFilePath):
9731017
ensureDirectoryExists(os.path.dirname(os.path.abspath(dstFilePath)))
974-
975-
if "CLCACHE_HARDLINK" in os.environ:
1018+
hardlink = GlobalSettings.getValue("CLCACHE_HARDLINK")
1019+
if hardlink and hardlink not in ['0', 'false', 'False']:
9761020
ret = windll.kernel32.CreateHardLinkW(str(dstFilePath), str(srcFilePath), None)
9771021
if ret != 0:
9781022
# Touch the time stamp of the new link so that the build system
@@ -998,11 +1042,10 @@ def myExecutablePath():
9981042

9991043

10001044
def findCompilerBinary():
1001-
if "CLCACHE_CL" in os.environ:
1002-
path = os.environ["CLCACHE_CL"]
1045+
path = GlobalSettings.getValue("CLCACHE_CL")
1046+
if path:
10031047
if os.path.basename(path) == path:
10041048
path = which(path)
1005-
10061049
return path if os.path.exists(path) else None
10071050

10081051
frozenByPy2Exe = hasattr(sys, "frozen")
@@ -1020,7 +1063,8 @@ def findCompilerBinary():
10201063

10211064

10221065
def printTraceStatement(msg: str) -> None:
1023-
if "CLCACHE_LOG" in os.environ:
1066+
clcachelog = GlobalSettings.getValue("CLCACHE_LOG")
1067+
if clcachelog and clcachelog not in ['0', 'false', 'False']:
10241068
scriptDir = os.path.realpath(os.path.dirname(sys.argv[0]))
10251069
with OUTPUT_LOCK:
10261070
print(os.path.join(scriptDir, "clcache.py") + " " + msg)
@@ -1570,8 +1614,8 @@ def main():
15701614

15711615
printTraceStatement("Found real compiler binary at '{0!s}'".format(compiler))
15721616
printTraceStatement("Arguments we care about: '{}'".format(sys.argv))
1573-
1574-
if "CLCACHE_DISABLE" in os.environ:
1617+
enabled = GlobalSettings.getValue("CLCACHE_DISABLE")
1618+
if enabled and enabled not in ['0', 'false', 'False']:
15751619
return invokeRealCompiler(compiler, sys.argv[1:])[0]
15761620
try:
15771621
return processCompileRequest(cache, compiler, sys.argv)
@@ -1670,8 +1714,8 @@ def processSingleSource(compiler, cmdLine, sourceFile, objectFile, environment):
16701714
try:
16711715
assert objectFile is not None
16721716
cache = Cache()
1673-
1674-
if 'CLCACHE_NODIRECT' in os.environ:
1717+
nodirect = GlobalSettings.getValue('CLCACHE_NODIRECT')
1718+
if nodirect and nodirect not in ['0', 'false', 'False']:
16751719
return processNoDirect(cache, objectFile, compiler, cmdLine, environment)
16761720
else:
16771721
return processDirect(cache, objectFile, compiler, cmdLine, sourceFile)
@@ -1770,7 +1814,8 @@ def ensureArtifactsExist(cache, cachekey, reason, objectFile, compilerResult, ex
17701814

17711815

17721816
if __name__ == '__main__':
1773-
if 'CLCACHE_PROFILE' in os.environ:
1817+
profilingEnabled = GlobalSettings.getValue('CLCACHE_PROFILE')
1818+
if profilingEnabled and profilingEnabled not in ['0', 'false', 'False']:
17741819
INVOCATION_HASH = getStringHash(','.join(sys.argv))
17751820
cProfile.run('main()', filename='clcache-{}.prof'.format(INVOCATION_HASH))
17761821
else:

tests/test_integration.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ def cd(targetDirectory):
4747
os.chdir(oldDirectory)
4848

4949

50+
def executeStatsCommand(customEnv=None):
51+
cmd = CLCACHE_CMD + ["-s"]
52+
if customEnv:
53+
out = subprocess.check_output(cmd, env=customEnv)
54+
else:
55+
out = subprocess.check_output(cmd)
56+
return extractStatsOutput(out.decode("ascii").strip())
57+
58+
59+
def extractStatsOutput(outputLines):
60+
stats = dict()
61+
print(outputLines)
62+
for line in outputLines.splitlines():
63+
kv = line.split(":", 1)
64+
if len(kv) != 2 or not kv[1]:
65+
continue
66+
stats[kv[0].strip()] = kv[1].strip()
67+
# special case to avoid duplication: Update 'Disc cache at X:\\blah\\ccache' => 'X:\\blah\\ccache'
68+
stats["current cache dir"] = stats["current cache dir"].split("cache at")[1].strip()
69+
return stats
70+
71+
5072
class TestCommandLineArguments(unittest.TestCase):
5173
def testValidMaxSize(self):
5274
with tempfile.TemporaryDirectory() as tempDir:
@@ -74,6 +96,87 @@ def testPrintStatistics(self):
7496
0,
7597
"Command must be able to print statistics")
7698

99+
100+
class TestGlobalSettings(unittest.TestCase):
101+
def testSettingsDefault(self):
102+
with tempfile.TemporaryDirectory() as tempDir:
103+
customEnv = dict(os.environ, HOME=tempDir)
104+
stats = executeStatsCommand(customEnv)
105+
print(stats)
106+
self.assertEqual(stats["current cache dir"], os.path.join(tempDir, "clcache"))
107+
108+
def testSettingsEnvironmentVariables(self):
109+
with tempfile.TemporaryDirectory() as tempDir:
110+
customEnv = dict(os.environ, CLCACHE_DIR=tempDir)
111+
stats = executeStatsCommand(customEnv)
112+
print(stats)
113+
self.assertEqual(stats["current cache dir"], os.path.join(tempDir))
114+
115+
def testSettingsLocalConfigFile(self):
116+
with tempfile.TemporaryDirectory() as tempDir:
117+
with cd(tempDir):
118+
confFileName = os.path.join(tempDir, "clcache.conf")
119+
clcacheDir = os.path.join(tempDir, "clcache")
120+
self._createConfFile(confFileName, CLCACHE_DIR=clcacheDir)
121+
stats = executeStatsCommand()
122+
self.assertEqual(stats["current cache dir"], clcacheDir)
123+
124+
def testConfigFileInHomeDir(self):
125+
with tempfile.TemporaryDirectory() as tempDir:
126+
confFileName = os.path.join(tempDir, ".clcache", "clcache.conf")
127+
clcacheDir = os.path.join(tempDir, "clcache")
128+
self._createConfFile(confFileName, CLCACHE_DIR=clcacheDir)
129+
customEnv = dict(os.environ, HOME=tempDir)
130+
stats = executeStatsCommand(customEnv)
131+
self.assertEqual(stats["current cache dir"], clcacheDir)
132+
133+
def testHomeDirOverridenByEnvironment(self):
134+
with tempfile.TemporaryDirectory() as tempDir:
135+
confFileName = os.path.join(tempDir, ".clcache", "clcache.conf")
136+
clcacheDir = os.path.join(tempDir, "clcache")
137+
self._createConfFile(confFileName, CLCACHE_DIR="this should be ignored")
138+
customEnv = dict(os.environ, HOME=tempDir, CLCACHE_DIR=clcacheDir)
139+
stats = executeStatsCommand(customEnv)
140+
self.assertEqual(stats["current cache dir"], clcacheDir)
141+
142+
def testSettingsConfigFileInProfiles(self):
143+
with tempfile.TemporaryDirectory() as tempDir:
144+
confFileName = os.path.join(tempDir, ".clcache", "clcache.conf")
145+
clcacheDir = os.path.join(tempDir, "clcache")
146+
self._createConfFile(confFileName, CLCACHE_DIR=clcacheDir)
147+
customEnv = dict(os.environ, HOME="blah", ALLUSERSPROFILE=tempDir)
148+
stats = executeStatsCommand(customEnv)
149+
self.assertEqual(stats["current cache dir"], clcacheDir)
150+
151+
def testConfProfilesOverridenByEnvironment(self):
152+
with tempfile.TemporaryDirectory() as tempDir:
153+
confFileName = os.path.join(tempDir, ".clcache", "clcache.conf")
154+
clcacheDir = os.path.join(tempDir, "clcache")
155+
self._createConfFile(confFileName, CLCACHE_DIR="should be ignored")
156+
customEnv = dict(os.environ, HOME="blah", ALLUSERSPROFILE=tempDir, CLCACHE_DIR=clcacheDir)
157+
stats = executeStatsCommand(customEnv)
158+
self.assertEqual(stats["current cache dir"], clcacheDir)
159+
160+
def testProfilesOverridenByHomeDir(self):
161+
with tempfile.TemporaryDirectory() as tempDir:
162+
clcacheDir = os.path.join(tempDir, "clcache")
163+
homeDir = os.path.join(tempDir, "home")
164+
self._createConfFile(os.path.join(homeDir, ".clcache", "clcache.conf"), CLCACHE_DIR=clcacheDir)
165+
profilesDir = os.path.join(tempDir, "allusersprofile")
166+
self._createConfFile(os.path.join(profilesDir, ".clcache", "clcache.conf"), CLCACHE_DIR="ignored")
167+
customEnv = dict(os.environ, HOME=homeDir, ALLUSERSPROFILE=profilesDir)
168+
stats = executeStatsCommand(customEnv)
169+
self.assertEqual(stats["current cache dir"], clcacheDir)
170+
171+
def _createConfFile(self, filename, **settings):
172+
dirname = os.path.dirname(filename)
173+
if not os.path.exists(dirname):
174+
os.makedirs(dirname)
175+
with open(filename, "w") as f:
176+
for k, v in settings.items():
177+
f.write("{0} = {1}\n\r".format(k, v))
178+
179+
77180
class TestDistutils(unittest.TestCase):
78181
@pytest.mark.skipif(not MONKEY_LOADED, reason="Monkeypatch not loaded")
79182
@pytest.mark.skipif(CLCACHE_MEMCACHED, reason="Fails with memcached")

0 commit comments

Comments
 (0)