diff --git a/SCons/Tool/MSCommon/MSVC/Config.py b/SCons/Tool/MSCommon/MSVC/Config.py index 29d6f246f2..7c0f1fe6ff 100644 --- a/SCons/Tool/MSCommon/MSVC/Config.py +++ b/SCons/Tool/MSCommon/MSVC/Config.py @@ -29,8 +29,6 @@ namedtuple, ) -from . import Util - from .Exceptions import ( MSVCInternalError, ) @@ -319,6 +317,7 @@ def verify(): + from . import Util from .. import vc for msvc_version in vc._VCVER: if msvc_version not in MSVC_VERSION_SUFFIX: diff --git a/SCons/Tool/MSCommon/MSVC/Registry.py b/SCons/Tool/MSCommon/MSVC/Registry.py index eee20ccbc7..f9e544c6fc 100644 --- a/SCons/Tool/MSCommon/MSVC/Registry.py +++ b/SCons/Tool/MSCommon/MSVC/Registry.py @@ -62,7 +62,7 @@ def registry_query_path(key, val, suffix, expand: bool=True): extval = val + '\\' + suffix if suffix else val qpath = read_value(key, extval, expand=expand) if qpath and os.path.exists(qpath): - qpath = Util.process_path(qpath) + qpath = Util.normalize_path(qpath) else: qpath = None return (qpath, key, val, extval) @@ -81,7 +81,7 @@ def microsoft_query_paths(suffix, usrval=None, expand: bool=True): extval = val + '\\' + suffix if suffix else val qpath = read_value(key, extval, expand=expand) if qpath and os.path.exists(qpath): - qpath = Util.process_path(qpath) + qpath = Util.normalize_path(qpath) if qpath not in paths: paths.append(qpath) records.append((qpath, key, val, extval, usrval)) diff --git a/SCons/Tool/MSCommon/MSVC/Util.py b/SCons/Tool/MSCommon/MSVC/Util.py index 64b8d673eb..6fd188bc35 100644 --- a/SCons/Tool/MSCommon/MSVC/Util.py +++ b/SCons/Tool/MSCommon/MSVC/Util.py @@ -26,16 +26,25 @@ """ import os +import pathlib import re from collections import ( namedtuple, ) +from ..common import debug + from . import Config # path utilities +# windows drive specification (e.g., 'C:') +_RE_DRIVESPEC = re.compile(r'^[A-Za-z][:]$', re.IGNORECASE) + +# windows path separators +_OS_PATH_SEPS = (os.path.sep, os.path.altsep) if os.path.altsep else (os.path.sep,) + def listdir_dirs(p): """ Return a list of tuples for each subdirectory of the given directory path. @@ -57,22 +66,92 @@ def listdir_dirs(p): dirs.append((dir_name, dir_path)) return dirs -def process_path(p): +def resolve_path(p, ignore_drivespec=True): """ - Normalize a system path + Make path absolute resolving any symlinks Args: p: str system path + ignore_drivespec: bool + ignore drive specifications when True Returns: - str: normalized system path + str: absolute path with symlinks resolved """ + if p: + + if ignore_drivespec and _RE_DRIVESPEC.match(p): + # don't attempt to resolve drive specification (e.g., C:) + pass + else: + # both abspath and resolve necessary for an unqualified file name + # on a mapped network drive in order to return a mapped drive letter + # path rather than a UNC path. + p = os.path.abspath(p) + try: + p = str(pathlib.Path(p).resolve()) + except OSError as e: + debug( + 'caught exception: path=%s, exception=%s(%s)', + repr(p), type(e).__name__, repr(str(e)) + ) + + return p + +def normalize_path( + p, + strip=True, + preserve_trailing=False, + expand=False, + realpath=True, + ignore_drivespec=True, +): + """ + Normalize path + + Args: + p: str + system path + strip: bool + remove leading and trailing whitespace when True + preserve_trailing: bool + preserve trailing path separator when True + expand: bool + apply expanduser and expandvars when True + realpath: bool + make the path absolute resolving any symlinks when True + ignore_drivespec: bool + ignore drive specifications for realpath when True + + Returns: + str: normalized path + + """ + + if p and strip: + p = p.strip() + + if p: + + trailing = bool(preserve_trailing and p.endswith(_OS_PATH_SEPS)) + + if expand: + p = os.path.expanduser(p) + p = os.path.expandvars(p) + p = os.path.normpath(p) - p = os.path.realpath(p) + + if realpath: + p = resolve_path(p, ignore_drivespec=ignore_drivespec) + p = os.path.normcase(p) + + if trailing: + p += os.path.sep + return p # msvc version and msvc toolset version regexes diff --git a/SCons/Tool/MSCommon/MSVC/UtilTests.py b/SCons/Tool/MSCommon/MSVC/UtilTests.py index 36e08f5eb1..86ea58d875 100644 --- a/SCons/Tool/MSCommon/MSVC/UtilTests.py +++ b/SCons/Tool/MSCommon/MSVC/UtilTests.py @@ -28,14 +28,46 @@ import unittest import os import re +import sys +import pathlib from SCons.Tool.MSCommon.MSVC import Config from SCons.Tool.MSCommon.MSVC import Util from SCons.Tool.MSCommon.MSVC import WinSDK +def resolve(p): + p = os.path.abspath(p) + p = str(pathlib.Path(p).resolve()) + return p + +def normalize(*comps): + p = os.path.join(*comps) + p = os.path.normpath(p) + p = os.path.normcase(p) + return p + class Data: - UTIL_PARENT_DIR = os.path.join(os.path.dirname(Util.__file__), os.pardir) + IS_WINDOWS = sys.platform == 'win32' + + CWD = os.getcwd() + + UTIL_MODULE = os.path.dirname(Util.__file__) + UTIL_MODULE_PARENT = os.path.join(UTIL_MODULE, os.pardir) + + HOME = pathlib.Path.home() + HOMEDRIVE = HOME.drive + HOMEPATH = str(HOME) + + REALPATH_CWD = resolve(CWD) + + REALPATH_UTIL_MODULE = resolve(UTIL_MODULE) + REALPATH_UTIL_MODULE_PARENT = resolve(UTIL_MODULE_PARENT) + + REALPATH_HOMEPATH = resolve(HOMEPATH) + REALPATH_HOMEPATH_PARENT = resolve(os.path.join(HOMEPATH, os.pardir)) + REALPATH_HOMEDRIVE = resolve(HOMEDRIVE) + REALPATH_HOMEDRIVE_CWD = resolve(HOMEDRIVE) class UtilTests(unittest.TestCase): @@ -43,21 +75,72 @@ def test_listdir_dirs(self) -> None: func = Util.listdir_dirs for dirname, expect in [ (None, False), ('', False), ('doesnotexist.xyz.abc', False), - (Data.UTIL_PARENT_DIR, True), + (Data.UTIL_MODULE_PARENT, True), ]: dirs = func(dirname) self.assertTrue((len(dirs) > 0) == expect, "{}({}): {}".format( func.__name__, repr(dirname), 'list is empty' if expect else 'list is not empty' )) - def test_process_path(self) -> None: - func = Util.process_path - for p, expect in [ - (None, True), ('', True), - ('doesnotexist.xyz.abc', False), (Data.UTIL_PARENT_DIR, False), - ]: - rval = func(p) - self.assertTrue((p == rval) == expect, "{}({}): {}".format( + def test_resolve_path(self) -> None: + func = Util.resolve_path + # default kwargs: + # ignore_drivespec=True + test_list = [ + (Data.UTIL_MODULE, Data.REALPATH_UTIL_MODULE, {}), + (os.path.join(Data.UTIL_MODULE, os.pardir), Data.REALPATH_UTIL_MODULE_PARENT, {}), + (Data.HOMEPATH, Data.REALPATH_HOMEPATH, {}), + (os.path.join(Data.HOMEPATH, os.pardir), Data.REALPATH_HOMEPATH_PARENT, {}), + ] + if Data.IS_WINDOWS: + test_list.extend([ + (Data.HOMEDRIVE, Data.HOMEDRIVE, {}), + (Data.HOMEDRIVE, Data.HOMEDRIVE, {'ignore_drivespec': True}), + (Data.HOMEDRIVE, Data.REALPATH_HOMEDRIVE_CWD, {'ignore_drivespec': False}), + ]) + for p, expect, kwargs in test_list: + rval = func(p, **kwargs) + # print(repr(p), repr(expect), repr(rval)) + self.assertTrue(rval == expect, "{}({}): {}".format( + func.__name__, repr(p), repr(rval) + )) + + def test_normalize_path(self) -> None: + func = Util.normalize_path + # default kwargs: + # strip=True + # preserve_trailing=False + # expand=False + # realpath=True + # ignore_drivespec=True + test_list = [ + (Data.UTIL_MODULE, normalize(Data.REALPATH_UTIL_MODULE), {}), + (os.path.join(Data.UTIL_MODULE, os.pardir), normalize(Data.REALPATH_UTIL_MODULE_PARENT), {}), + (None, None, {}), + ('', '', {'realpath': False}), + ('', '', {'realpath': True}), + ('DoesNotExist.xyz.abc', normalize('DoesNotExist.xyz.abc'), {'realpath': False}), + ('DoesNotExist.xyz.abc', normalize(Data.REALPATH_CWD, 'DoesNotExist.xyz.abc'), {'realpath': True}), + (' DoesNotExist.xyz.abc ', normalize(Data.REALPATH_CWD, 'DoesNotExist.xyz.abc'), {'realpath': True}), + (' ~ ', '~', {'realpath': False, 'expand': False}), + (' ~ ', normalize(Data.REALPATH_HOMEPATH), {'realpath': True, 'expand': True}), + ] + if Data.IS_WINDOWS: + test_list.extend([ + ('DoesNotExist.xyz.abc/', normalize('DoesNotExist.xyz.abc') + os.path.sep, {'realpath': False, 'preserve_trailing': True}), + (' DoesNotExist.xyz.abc\\ ', normalize('DoesNotExist.xyz.abc') + os.path.sep, {'realpath': False, 'preserve_trailing': True}), + (' ~/ ', normalize(Data.REALPATH_HOMEPATH) + os.path.sep, {'realpath': True, 'expand': True, 'preserve_trailing': True}), + (' ~\\ ', normalize(Data.REALPATH_HOMEPATH) + os.path.sep, {'realpath': True, 'expand': True, 'preserve_trailing': True}), + (' ~/ ', normalize(Data.REALPATH_CWD, '~') + os.path.sep, {'realpath': True, 'expand': False, 'preserve_trailing': True}), + (' ~\\ ', normalize(Data.REALPATH_CWD, '~') + os.path.sep, {'realpath': True, 'expand': False, 'preserve_trailing': True}), + (Data.HOMEDRIVE, normalize(Data.HOMEDRIVE), {}), + (Data.HOMEDRIVE, normalize(Data.HOMEDRIVE), {'ignore_drivespec': True}), + (Data.HOMEDRIVE, normalize(Data.REALPATH_HOMEDRIVE_CWD), {'ignore_drivespec': False}), + ]) + for p, expect, kwargs in test_list: + rval = func(p, **kwargs) + # print(repr(p), repr(expect), repr(rval)) + self.assertTrue(rval == expect, "{}({}): {}".format( func.__name__, repr(p), repr(rval) )) diff --git a/SCons/Tool/MSCommon/MSVC/WinSDK.py b/SCons/Tool/MSCommon/MSVC/WinSDK.py index 39617b16cc..7115d505ee 100644 --- a/SCons/Tool/MSCommon/MSVC/WinSDK.py +++ b/SCons/Tool/MSCommon/MSVC/WinSDK.py @@ -83,7 +83,7 @@ def _sdk_10_layout(version): if not version_nbr.startswith(folder_prefix): continue - sdk_inc_path = Util.process_path(os.path.join(version_nbr_path, 'um')) + sdk_inc_path = Util.normalize_path(os.path.join(version_nbr_path, 'um')) if not os.path.exists(sdk_inc_path): continue @@ -127,7 +127,7 @@ def _sdk_81_layout(version): # msvc does not check for existence of root or other files - sdk_inc_path = Util.process_path(os.path.join(sdk_root, r'include\um')) + sdk_inc_path = Util.normalize_path(os.path.join(sdk_root, r'include\um')) if not os.path.exists(sdk_inc_path): continue