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

Commit 89c7c61

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) is used, or 2. [current_working_dir]/clcache.conf is loaded, or, if not found 3. [%USERPROFILE% or ~/].clcache/clcache.conf is be loaded, or if not found 4. [%ALLUSERSPROFILE% or /etc/].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 7a3e62a commit 89c7c61

File tree

3 files changed

+169
-20
lines changed

3 files changed

+169
-20
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: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,49 @@ def getIncludesContentHashForHashes(listOfHashes):
308308
return HashAlgorithm(','.join(listOfHashes).encode()).hexdigest()
309309

310310

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

360403
@staticmethod
361404
def forPath(path):
362-
timeoutMs = int(os.environ.get('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
405+
timeoutMs = int(GlobalSettings.getValue('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
363406
lockName = path.replace(':', '-').replace('\\', '-')
364407
return CacheLock(lockName, timeoutMs)
365408

@@ -505,10 +548,8 @@ class CacheFileStrategy(object):
505548
def __init__(self, cacheDirectory=None):
506549
self.dir = cacheDirectory
507550
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")
551+
self.dir = GlobalSettings.getValue("CLCACHE_DIR",
552+
os.path.join(os.path.expanduser("~"), "clcache"))
512553

513554
manifestsRootDir = os.path.join(self.dir, "manifests")
514555
ensureDirectoryExists(manifestsRootDir)
@@ -593,9 +634,10 @@ def clean(self, stats, maximumSize):
593634

594635
class Cache(object):
595636
def __init__(self, cacheDirectory=None):
596-
if os.environ.get("CLCACHE_MEMCACHED"):
637+
memcached = GlobalSettings.getValue("CLCACHE_MEMCACHED")
638+
if memcached:
597639
from .storage import CacheFileWithMemcacheFallbackStrategy
598-
self.strategy = CacheFileWithMemcacheFallbackStrategy(os.environ.get("CLCACHE_MEMCACHED"),
640+
self.strategy = CacheFileWithMemcacheFallbackStrategy(memcached,
599641
cacheDirectory=cacheDirectory)
600642
else:
601643
self.strategy = CacheFileStrategy(cacheDirectory=cacheDirectory)
@@ -900,7 +942,7 @@ def getCompilerHash(compilerBinary):
900942

901943

902944
def getFileHashes(filePaths):
903-
if 'CLCACHE_SERVER' in os.environ:
945+
if GlobalSettings.getValue('CLCACHE_SERVER') not in ['0', 'false', 'False']:
904946
pipeName = r'\\.\pipe\clcache_srv'
905947
while True:
906948
try:
@@ -939,7 +981,7 @@ def getStringHash(dataString):
939981

940982

941983
def expandBasedirPlaceholder(path):
942-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
984+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
943985
if path.startswith(BASEDIR_REPLACEMENT):
944986
if not baseDir:
945987
raise LogicException('No CLCACHE_BASEDIR set, but found relative path ' + path)
@@ -949,7 +991,7 @@ def expandBasedirPlaceholder(path):
949991

950992

951993
def collapseBasedirToPlaceholder(path):
952-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
994+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
953995
if baseDir is None:
954996
return path
955997
else:
@@ -972,7 +1014,7 @@ def ensureDirectoryExists(path):
9721014
def copyOrLink(srcFilePath, dstFilePath):
9731015
ensureDirectoryExists(os.path.dirname(os.path.abspath(dstFilePath)))
9741016

975-
if "CLCACHE_HARDLINK" in os.environ:
1017+
if GlobalSettings.getValue("CLCACHE_HARDLINK") not in ['0', 'false', 'False']:
9761018
ret = windll.kernel32.CreateHardLinkW(str(dstFilePath), str(srcFilePath), None)
9771019
if ret != 0:
9781020
# Touch the time stamp of the new link so that the build system
@@ -998,11 +1040,10 @@ def myExecutablePath():
9981040

9991041

10001042
def findCompilerBinary():
1001-
if "CLCACHE_CL" in os.environ:
1002-
path = os.environ["CLCACHE_CL"]
1043+
path = GlobalSettings.getValue("CLCACHE_CL")
1044+
if path:
10031045
if os.path.basename(path) == path:
10041046
path = which(path)
1005-
10061047
return path if os.path.exists(path) else None
10071048

10081049
frozenByPy2Exe = hasattr(sys, "frozen")
@@ -1020,7 +1061,7 @@ def findCompilerBinary():
10201061

10211062

10221063
def printTraceStatement(msg: str) -> None:
1023-
if "CLCACHE_LOG" in os.environ:
1064+
if GlobalSettings.getValue("CLCACHE_LOG") not in ['0', 'false', 'False']:
10241065
scriptDir = os.path.realpath(os.path.dirname(sys.argv[0]))
10251066
with OUTPUT_LOCK:
10261067
print(os.path.join(scriptDir, "clcache.py") + " " + msg)
@@ -1571,7 +1612,7 @@ def main():
15711612
printTraceStatement("Found real compiler binary at '{0!s}'".format(compiler))
15721613
printTraceStatement("Arguments we care about: '{}'".format(sys.argv))
15731614

1574-
if "CLCACHE_DISABLE" in os.environ:
1615+
if GlobalSettings.getValue("CLCACHE_DISABLE") not in ['0', 'false', 'False']:
15751616
return invokeRealCompiler(compiler, sys.argv[1:])[0]
15761617
try:
15771618
return processCompileRequest(cache, compiler, sys.argv)
@@ -1671,7 +1712,7 @@ def processSingleSource(compiler, cmdLine, sourceFile, objectFile, environment):
16711712
assert objectFile is not None
16721713
cache = Cache()
16731714

1674-
if 'CLCACHE_NODIRECT' in os.environ:
1715+
if GlobalSettings.getValue('CLCACHE_NODIRECT') not in ['0', 'false', 'False']:
16751716
return processNoDirect(cache, objectFile, compiler, cmdLine, environment)
16761717
else:
16771718
return processDirect(cache, objectFile, compiler, cmdLine, sourceFile)
@@ -1770,7 +1811,7 @@ def ensureArtifactsExist(cache, cachekey, reason, objectFile, compilerResult, ex
17701811

17711812

17721813
if __name__ == '__main__':
1773-
if 'CLCACHE_PROFILE' in os.environ:
1814+
if GlobalSettings.getValue('CLCACHE_PROFILE') not in ['0', 'false', 'False']:
17741815
INVOCATION_HASH = getStringHash(','.join(sys.argv))
17751816
cProfile.run('main()', filename='clcache-{}.prof'.format(INVOCATION_HASH))
17761817
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 testSettingsConfigFileInHomeDir(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 testSettingsConfigFileInHomeDirOverridenByEnvironment(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 testSettingsConfigFileInProfilesOverridenByEnvironment(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 testSettingsConfigFileInProfilesOverridenByHomeDirSettings(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)