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

Commit b05cf8c

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 b05cf8c

File tree

3 files changed

+171
-21
lines changed

3 files changed

+171
-21
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: 61 additions & 19 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:
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,7 @@ def getCompilerHash(compilerBinary):
900943

901944

902945
def getFileHashes(filePaths):
903-
if 'CLCACHE_SERVER' in os.environ:
946+
if GlobalSettings.getValue('CLCACHE_SERVER') not in [None, '0', 'false', 'False']:
904947
pipeName = r'\\.\pipe\clcache_srv'
905948
while True:
906949
try:
@@ -939,7 +982,7 @@ def getStringHash(dataString):
939982

940983

941984
def expandBasedirPlaceholder(path):
942-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
985+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
943986
if path.startswith(BASEDIR_REPLACEMENT):
944987
if not baseDir:
945988
raise LogicException('No CLCACHE_BASEDIR set, but found relative path ' + path)
@@ -949,7 +992,7 @@ def expandBasedirPlaceholder(path):
949992

950993

951994
def collapseBasedirToPlaceholder(path):
952-
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
995+
baseDir = normalizeBaseDir(GlobalSettings.getValue('CLCACHE_BASEDIR'))
953996
if baseDir is None:
954997
return path
955998
else:
@@ -972,7 +1015,7 @@ def ensureDirectoryExists(path):
9721015
def copyOrLink(srcFilePath, dstFilePath):
9731016
ensureDirectoryExists(os.path.dirname(os.path.abspath(dstFilePath)))
9741017

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

9991042

10001043
def findCompilerBinary():
1001-
if "CLCACHE_CL" in os.environ:
1002-
path = os.environ["CLCACHE_CL"]
1044+
path = GlobalSettings.getValue("CLCACHE_CL")
1045+
if path:
10031046
if os.path.basename(path) == path:
10041047
path = which(path)
1005-
10061048
return path if os.path.exists(path) else None
10071049

10081050
frozenByPy2Exe = hasattr(sys, "frozen")
@@ -1020,7 +1062,7 @@ def findCompilerBinary():
10201062

10211063

10221064
def printTraceStatement(msg: str) -> None:
1023-
if "CLCACHE_LOG" in os.environ:
1065+
if GlobalSettings.getValue("CLCACHE_LOG") not in [None, '0', 'false', 'False']:
10241066
scriptDir = os.path.realpath(os.path.dirname(sys.argv[0]))
10251067
with OUTPUT_LOCK:
10261068
print(os.path.join(scriptDir, "clcache.py") + " " + msg)
@@ -1571,7 +1613,7 @@ def main():
15711613
printTraceStatement("Found real compiler binary at '{0!s}'".format(compiler))
15721614
printTraceStatement("Arguments we care about: '{}'".format(sys.argv))
15731615

1574-
if "CLCACHE_DISABLE" in os.environ:
1616+
if GlobalSettings.getValue("CLCACHE_DISABLE") not in [None, '0', 'false', 'False']:
15751617
return invokeRealCompiler(compiler, sys.argv[1:])[0]
15761618
try:
15771619
return processCompileRequest(cache, compiler, sys.argv)
@@ -1671,7 +1713,7 @@ def processSingleSource(compiler, cmdLine, sourceFile, objectFile, environment):
16711713
assert objectFile is not None
16721714
cache = Cache()
16731715

1674-
if 'CLCACHE_NODIRECT' in os.environ:
1716+
if GlobalSettings.getValue('CLCACHE_NODIRECT') not in [None, '0', 'false', 'False']:
16751717
return processNoDirect(cache, objectFile, compiler, cmdLine, environment)
16761718
else:
16771719
return processDirect(cache, objectFile, compiler, cmdLine, sourceFile)
@@ -1770,7 +1812,7 @@ def ensureArtifactsExist(cache, cachekey, reason, objectFile, compilerResult, ex
17701812

17711813

17721814
if __name__ == '__main__':
1773-
if 'CLCACHE_PROFILE' in os.environ:
1815+
if GlobalSettings.getValue('CLCACHE_PROFILE') not in [None, '0', 'false', 'False']:
17741816
INVOCATION_HASH = getStringHash(','.join(sys.argv))
17751817
cProfile.run('main()', filename='clcache-{}.prof'.format(INVOCATION_HASH))
17761818
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)