diff --git a/CHANGES.txt b/CHANGES.txt index 671530bee8..e323e677f9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -150,6 +150,10 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER file an error. It has issued a warning since 3.0, with "warn instead of fail" deprecated since 3.1. Fixes #3958. - Minor (non-functional) cleanup of some tests, particuarly test/MSVC. + - Added more error handling while reading msvc config cache. + (Enabled/specified by SCONS_CACHE_MSVC_CONFIG). + The existing cache will be discarded if there's a decode error reading it. + It's possible there's a race condition creating this issue it in certain CI builds. From Jonathon Reinhart: - Fix another instance of `int main()` in CheckLib() causing failures diff --git a/RELEASE.txt b/RELEASE.txt index 9c109e30f7..80419497da 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -103,7 +103,10 @@ FIXES - FORTRAN: Fix gfortran tool initialization. Defaults to using binary named gfortran as would be expected, and properly set's SHFORTRAN flags to include -fPIC where previously it was only doing so for the other fortran versions (F77,..) - +- MSCommon: Added more error handling while reading msvc config cache. + (Enabled/specified by SCONS_CACHE_MSVC_CONFIG). + The existing cache will be discarded if there's a decode error reading it. + It's possible there's a race condition creating this issue it in certain CI builds. IMPROVEMENTS ------------ diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py index f497f1abfe..9ab7179ee8 100644 --- a/SCons/Tool/MSCommon/common.py +++ b/SCons/Tool/MSCommon/common.py @@ -103,7 +103,7 @@ def debug(x, *args): # SCONS_CACHE_MSVC_CONFIG is public, and is documented. -CONFIG_CACHE = os.environ.get('SCONS_CACHE_MSVC_CONFIG') +CONFIG_CACHE = os.environ.get('SCONS_CACHE_MSVC_CONFIG', '') if CONFIG_CACHE in ('1', 'true', 'True'): CONFIG_CACHE = os.path.join(os.path.expanduser('~'), 'scons_msvc_cache.json') @@ -113,50 +113,59 @@ def debug(x, *args): if os.environ.get('SCONS_CACHE_MSVC_FORCE_DEFAULTS') in ('1', 'true', 'True'): CONFIG_CACHE_FORCE_DEFAULT_ARGUMENTS = True -def read_script_env_cache(): +def read_script_env_cache() -> dict: """ fetch cached msvc env vars if requested, else return empty dict """ envcache = {} - if CONFIG_CACHE: + p = Path(CONFIG_CACHE) + if not CONFIG_CACHE or not p.is_file(): + return envcache + with p.open('r') as f: + # Convert the list of cache entry dictionaries read from + # json to the cache dictionary. Reconstruct the cache key + # tuple from the key list written to json. try: - p = Path(CONFIG_CACHE) - with p.open('r') as f: - # Convert the list of cache entry dictionaries read from - # json to the cache dictionary. Reconstruct the cache key - # tuple from the key list written to json. - envcache_list = json.load(f) - if isinstance(envcache_list, list): - envcache = {tuple(d['key']): d['data'] for d in envcache_list} - else: - # don't fail if incompatible format, just proceed without it - warn_msg = "Incompatible format for msvc cache file {}: file may be overwritten.".format( - repr(CONFIG_CACHE) - ) - SCons.Warnings.warn(MSVCCacheInvalidWarning, warn_msg) - debug(warn_msg) - except FileNotFoundError: - # don't fail if no cache file, just proceed without it - pass + envcache_list = json.load(f) + except json.JSONDecodeError: + # If we couldn't decode it, it could be corrupt. Toss. + with suppress(FileNotFoundError): + p.unlink() + warn_msg = "Could not decode msvc cache file %s: dropping." + SCons.Warnings.warn(MSVCCacheInvalidWarning, warn_msg % CONFIG_CACHE) + debug(warn_msg, CONFIG_CACHE) + else: + if isinstance(envcache_list, list): + envcache = {tuple(d['key']): d['data'] for d in envcache_list} + else: + # don't fail if incompatible format, just proceed without it + warn_msg = "Incompatible format for msvc cache file %s: file may be overwritten." + SCons.Warnings.warn(MSVCCacheInvalidWarning, warn_msg % CONFIG_CACHE) + debug(warn_msg, CONFIG_CACHE) + return envcache def write_script_env_cache(cache) -> None: """ write out cache of msvc env vars if requested """ - if CONFIG_CACHE: - try: - p = Path(CONFIG_CACHE) - with p.open('w') as f: - # Convert the cache dictionary to a list of cache entry - # dictionaries. The cache key is converted from a tuple to - # a list for compatibility with json. - envcache_list = [{'key': list(key), 'data': data} for key, data in cache.items()] - json.dump(envcache_list, f, indent=2) - except TypeError: - # data can't serialize to json, don't leave partial file - with suppress(FileNotFoundError): - p.unlink() - except OSError: - # can't write the file, just skip - pass + if not CONFIG_CACHE: + return + + p = Path(CONFIG_CACHE) + try: + with p.open('w') as f: + # Convert the cache dictionary to a list of cache entry + # dictionaries. The cache key is converted from a tuple to + # a list for compatibility with json. + envcache_list = [{'key': list(key), 'data': data} for key, data in cache.items()] + json.dump(envcache_list, f, indent=2) + except TypeError: + # data can't serialize to json, don't leave partial file + with suppress(FileNotFoundError): + p.unlink() + except OSError: + # can't write the file, just skip + pass + + return _is_win64 = None @@ -381,7 +390,7 @@ def parse_output(output, keep=KEEPLIST): # rdk will keep the regex to match the .bat file output line starts rdk = {} for i in keep: - rdk[i] = re.compile('%s=(.*)' % i, re.I) + rdk[i] = re.compile(r'%s=(.*)' % i, re.I) def add_env(rmatch, key, dkeep=dkeep) -> None: path_list = rmatch.group(1).split(os.pathsep)