diff --git a/src/rez/config.py b/src/rez/config.py index adfdfe1c2..a2ab31bd9 100644 --- a/src/rez/config.py +++ b/src/rez/config.py @@ -345,6 +345,8 @@ def _parse_env_var(self, value): "release_hooks": StrList, "context_tracking_context_fields": StrList, "pathed_env_vars": StrList, + "shell_pathed_env_vars": OptionalDict, + "disable_normalization": OptionalBool, "prompt_release_message": Bool, "critical_styles": OptionalStrList, "error_styles": OptionalStrList, diff --git a/src/rez/rex.py b/src/rez/rex.py index ac76c143d..18f7e074a 100644 --- a/src/rez/rex.py +++ b/src/rez/rex.py @@ -276,6 +276,16 @@ def _value(self, value): expanded_value = self._expand(unexpanded_value) return unexpanded_value, expanded_value + def _implicit_value(self, value): + def _fn(str_): + str_ = expandvars(str_, self.environ) + str_ = expandvars(str_, self.parent_environ) + return str_ + + unexpanded_value = self._format(value) + expanded_value = EscapedString.promote(value).formatted(_fn) + return unexpanded_value, expanded_value + def get_output(self, style=OutputStyle.file): return self.interpreter.get_output(style=style) @@ -307,7 +317,10 @@ def getenv(self, key): def setenv(self, key, value): unexpanded_key, expanded_key = self._key(key) - unexpanded_value, expanded_value = self._value(value) + if key == "REZ_USED_IMPLICIT_PACKAGES": + unexpanded_value, expanded_value = self._implicit_value(value) + else: + unexpanded_value, expanded_value = self._value(value) # TODO: check if value has already been set by another package self.actions.append(Setenv(unexpanded_key, unexpanded_value)) @@ -543,7 +556,7 @@ def shebang(self): # --- other - def escape_string(self, value, is_path=False): + def escape_string(self, value, is_path=False, is_shell_path=False): """Escape a string. Escape the given string so that special characters (such as quotes and @@ -561,6 +574,7 @@ def escape_string(self, value, is_path=False): Args: value (str or `EscapedString`): String to escape. is_path (bool): True if the value is path-like. + is_shell_path (bool): True if the value is a shell-path. Returns: str: The escaped string. @@ -571,6 +585,16 @@ def escape_string(self, value, is_path=False): def _is_pathed_key(cls, key): return any(fnmatch(key, x) for x in config.pathed_env_vars) + @classmethod + def _is_shell_pathed_key(cls, key): + shell_name = cls.name() if hasattr(cls, 'name') else '' + if shell_name not in config.shell_pathed_env_vars: + return False + + return any( + fnmatch(key, x) for x in config.shell_pathed_env_vars[shell_name] + ) + def normalize_path(self, path): """Normalize a path. @@ -801,6 +825,45 @@ def _add_systemroot_to_env_win32(self, env): env['SYSTEMROOT'] = os.environ['SYSTEMROOT'] + def as_path(self, path): + """ + Return the given path as a system path. + Used if the path needs to be reformatted to suit a specific case. + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + return path + + def as_shell_path(self, path): + """ + Return the given path as a shell path. + Used if the shell requires a different pathing structure. + + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + return path + + def normalize_path(self, path): + """ + Normalize the path to fit the environment. + For example, POSIX paths, Windows path, etc. If no transformation is + necessary, just return the path. + + Args: + path (str): File path. + + Returns: + (str): Normalized file path. + """ + return path + #=============================================================================== # String manipulation @@ -1359,7 +1422,7 @@ def normalize_path(self, path): Returns: str: The normalized path. """ - return self.interpreter.normalize_path(path) + return self.interpreter.as_path(path) @classmethod def compile_code(cls, code, filename=None, exec_namespace=None): diff --git a/src/rez/rex_bindings.py b/src/rez/rex_bindings.py index 62b45acb6..2aea8387f 100644 --- a/src/rez/rex_bindings.py +++ b/src/rez/rex_bindings.py @@ -132,7 +132,7 @@ def root(self): root = self.__cached_root or self.__variant.root if self.__interpreter: - root = self.__interpreter.normalize_path(root) + root = self.__interpreter.as_path(root) return root diff --git a/src/rez/rezconfig.py b/src/rez/rezconfig.py index 93e5f7a66..f58653cd5 100644 --- a/src/rez/rezconfig.py +++ b/src/rez/rezconfig.py @@ -534,6 +534,19 @@ "*PATH" ] +# Some shells may require multiple types of pathing, so this option provides +# a way to define variables on a per-shell basis to convert for shell pathing +# instead of the pathing provided above or no modification at all. +shell_pathed_env_vars = { + "gitbash": ["PYTHONPATH"] +} + +# If set to True, completely disables any path transformations that would occur +# as a result of both the shell and the settings in "pathed_env_vars" and +# "shell_pathed_env_vars". This is meant to aid in debugging and should be +# False unless needed. +disable_normalization = False + # Defines what suites on $PATH stay visible when a new rez environment is resolved. # Possible values are: # - "never": Don"t attempt to keep any suites visible in a new env diff --git a/src/rez/shells.py b/src/rez/shells.py index f4741ad47..297ca4812 100644 --- a/src/rez/shells.py +++ b/src/rez/shells.py @@ -318,6 +318,45 @@ def join(cls, command): return shlex_join(command, replacements=replacements) + def as_path(self, path): + """ + Return the given path as a system path. + Used if the path needs to be reformatted to suit a specific case. + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + return path + + def as_shell_path(self, path): + """ + Return the given path as a shell path. + Used if the shell requires a different pathing structure. + + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + return path + + def normalize_path(self, path): + """ + Normalize the path to fit the environment. + For example, POSIX paths, Windows path, etc. If no transformation is + necessary, just return the path. + + Args: + path (str): File path. + + Returns: + (str): Normalized file path. + """ + return path + class UnixShell(Shell): """ @@ -433,7 +472,7 @@ def _create_ex(): assert self.rcfile_arg shell_command = "%s %s" % (self.executable, self.rcfile_arg) else: - shell_command = self.executable + shell_command = '"{}"'.format(self.executable) if do_rcfile: # hijack rcfile to insert our own script diff --git a/src/rez/tests/test_shell_utils.py b/src/rez/tests/test_shell_utils.py new file mode 100644 index 000000000..63bd6534b --- /dev/null +++ b/src/rez/tests/test_shell_utils.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +"""Tests for shell utils.""" + +from rez.tests.util import TestBase +from rezplugins.shell._utils.windows import convert_path + + +class ShellUtils(TestBase): + """Test shell util functions.""" + + def test_path_conversion_windows(self): + """Test the path conversion to windows style.""" + test_path = r'C:\foo/bar/spam' + converted_path = convert_path(test_path, 'windows') + expected_path = r'C:\foo\bar\spam' + + self.assertEqual(converted_path, expected_path) + + def test_path_conversion_unix(self): + """Test the path conversion to unix style.""" + test_path = r'C:\foo\bar\spam' + converted_path = convert_path(test_path, 'unix') + expected_path = r'/c/foo/bar/spam' + + self.assertEqual(converted_path, expected_path) + + def test_path_conversion_mixed(self): + """Test the path conversion to mixed style.""" + test_path = r'C:\foo\bar\spam' + converted_path = convert_path(test_path, 'unix') + expected_path = r'/c/foo/bar/spam' + + self.assertEqual(converted_path, expected_path) + + def test_path_conversion_unix_forced_fwdslash(self): + """Test the path conversion to unix style.""" + test_path = r'C:\foo\bar\spam' + converted_path = convert_path(test_path, 'unix', force_fwdslash=True) + expected_path = r'/c/foo/bar/spam' + + self.assertEqual(converted_path, expected_path) + + def test_path_conversion_mixed_forced_fwdslash(self): + """Test the path conversion to mixed style.""" + test_path = r'C:\foo\bar\spam' + converted_path = convert_path(test_path, 'mixed', force_fwdslash=True) + expected_path = r'C:/foo/bar/spam' + + self.assertEqual(converted_path, expected_path) diff --git a/src/rez/tests/test_shells.py b/src/rez/tests/test_shells.py index f131b212e..0fbb5a474 100644 --- a/src/rez/tests/test_shells.py +++ b/src/rez/tests/test_shells.py @@ -504,6 +504,17 @@ def _make_alias(ex): out, _ = p.communicate() self.assertEqual(0, p.returncode) + @per_available_shell() + def test_disabled_path_normalization(self, shell): + """Test disabling path normalization via the config.""" + config.override('disable_normalization', True) + + test_path = r'C:\foo\bar\spam' + normalized_path = shell.normalize_path(test_path) + expected_path = r'C:\foo\bar\spam' + + self.assertEqual(normalized_path, expected_path) + if __name__ == '__main__': unittest.main() diff --git a/src/rez/utils/sourcecode.py b/src/rez/utils/sourcecode.py index 42c7ed63d..cbc270b79 100644 --- a/src/rez/utils/sourcecode.py +++ b/src/rez/utils/sourcecode.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project +import re from rez.utils.formatting import indent from rez.utils.data_utils import cached_property @@ -12,6 +13,8 @@ import traceback import os.path +_drive_start_regex = re.compile(r"^([A-Za-z]):\\") + def early(): """Used by functions in package.py to harden to the return value at build time. @@ -97,7 +100,18 @@ def __init__(self, source=None, func=None, filepath=None, eval_as_function=True): self.source = (source or '').rstrip() self.func = func + self.filepath = filepath + if self.filepath: + drive_letter_match = _drive_start_regex.match(filepath) + # If converting the drive letter to posix, lowercase the drive + # letter as per cygpath behavior. + if drive_letter_match: + self.filepath = _drive_start_regex.sub( + drive_letter_match.expand("/\\1/").lower(), filepath + ) + self.filepath = self.filepath.replace("\\", "/") + self.eval_as_function = eval_as_function self.package = None diff --git a/src/rezplugins/shell/_utils/powershell_base.py b/src/rezplugins/shell/_utils/powershell_base.py index 70e43f867..051d74d49 100644 --- a/src/rezplugins/shell/_utils/powershell_base.py +++ b/src/rezplugins/shell/_utils/powershell_base.py @@ -13,8 +13,9 @@ from rez.system import system from rez.utils.platform_ import platform_ from rez.utils.execution import Popen +from rez.utils.logging_ import print_debug from rez.util import shlex_join -from .windows import to_windows_path +from .windows import convert_path class PowerShellBase(Shell): @@ -226,7 +227,7 @@ def get_output(self, style=OutputStyle.file): script = '&& '.join(lines) return script - def escape_string(self, value, is_path=False): + def escape_string(self, value, is_path=False, is_shell_path=False): value = EscapedString.promote(value) value = value.expanduser() result = '' @@ -237,14 +238,29 @@ def escape_string(self, value, is_path=False): else: if is_path: txt = self.normalize_paths(txt) + elif is_shell_path: + txt = self.as_shell_path(txt) txt = self._escape_quotes(txt) result += txt return result def normalize_path(self, path): + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + if platform_.name == "windows": - return to_windows_path(path) + converted_path = convert_path(path, 'windows') + if path != converted_path: + print_debug( + 'Path converted: {} -> {}'.format(path, converted_path) + ) + self._addline( + '# Path converted: {} -> {}'.format(path, converted_path) + ) + return converted_path + else: return path @@ -255,21 +271,45 @@ def shebang(self): pass def setenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) - self._addline('Set-Item -Path "Env:{0}" -Value "{1}"'.format(key, value)) + is_path = self._is_pathed_key(key) + new_value = self.escape_string(value, is_path=is_path) + + if is_path and value != new_value: + print_debug( + 'Path changed: {} -> {}'.format(value, new_value) + ) + self._addline( + '# Path value changed: {} -> {}'.format(value, new_value) + ) + + self._addline( + 'Set-Item -Path "Env:{0}" -Value "{1}"'.format(key, new_value) + ) def prependenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) + is_path = self._is_pathed_key(key) + new_value = self.escape_string(value, is_path=is_path) + + if is_path and value != new_value: + self._addline( + '# Path value changed: {} -> {}'.format(value, new_value) + ) # Be careful about ambiguous case in pwsh on Linux where pathsep is : # so that the ${ENV:VAR} form has to be used to not collide. self._addline( 'Set-Item -Path "Env:{0}" -Value ("{1}{2}" + (Get-ChildItem "Env:{0}").Value)'.format( - key, value, self.pathsep) + key, new_value, self.pathsep) ) def appendenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) + is_path = self._is_pathed_key(key) + new_value = self.escape_string(value, is_path=is_path) + + if is_path and value != new_value: + self._addline( + '# Path value changed: {} -> {}'.format(value, new_value) + ) # Be careful about ambiguous case in pwsh on Linux where pathsep is : # so that the ${ENV:VAR} form has to be used to not collide. diff --git a/src/rezplugins/shell/_utils/windows.py b/src/rezplugins/shell/_utils/windows.py index 924c64f3a..329cfeee1 100644 --- a/src/rezplugins/shell/_utils/windows.py +++ b/src/rezplugins/shell/_utils/windows.py @@ -6,19 +6,29 @@ import re import subprocess from rez.utils.execution import Popen - +from rez.utils.logging_ import print_debug _drive_start_regex = re.compile(r"^([A-Za-z]):\\") +_drive_regex_mixed = re.compile(r"([a-z]):/") _env_var_regex = re.compile(r"%([^%]*)%") -def to_posix_path(path): - """Convert (eg) "C:\foo" to "/c/foo" +def convert_path(path, mode='unix', force_fwdslash=False): + r"""Convert a path to unix style or windows style as per cygpath rules. - TODO: doesn't take into account escaped bask slashes, which would be - weird to have in a path, but is possible. - """ + Args: + path (str): Path to convert. + mode (str|Optional): Cygpath-style mode to use: + unix (default): Unix style path (c:\ and C:\ -> /c/) + mixed: Windows style drives with forward slashes + (c:\ and C:\ -> C:/) + windows: Windows style paths (C:\) + force_fwdslash (bool|Optional): Return a path containing only + forward slashes regardless of mode. Default is False. + Returns: + path(str): Converted path. + """ # expand refs like %SYSTEMROOT%, leave as-is if not in environ def _repl(m): varname = m.groups()[0] @@ -26,17 +36,53 @@ def _repl(m): path = _env_var_regex.sub(_repl, path) - # C:\ ==> /C/ - path = _drive_start_regex.sub("/\\1/", path) + # Convert the path based on mode. + if mode == 'mixed': + new_path = to_mixed_path(path) + elif mode == 'windows': + new_path = to_windows_path(path) + else: + new_path = to_posix_path(path) + + # NOTE: This would be normal cygpath behavior, but the broader + # implications of enabling it need extensive testing. + # Leaving it up to the user for now. + if force_fwdslash: + # Backslash -> fwdslash + new_path = new_path.replace('\\', '/') + + if path != new_path: + print_debug('Path converted: {} -> {}'.format(path, new_path)) + + return new_path + + +def to_posix_path(path): + """Convert (eg) "C:\foo" to "/c/foo" + + TODO: doesn't take into account escaped bask slashes, which would be + weird to have in a path, but is possible. + + Args: + path (str): Path to convert. + """ + # c:\ and C:\ -> /c/ + drive_letter_match = _drive_start_regex.match(path) + # If converting the drive letter to posix, capitalize the drive + # letter as per cygpath behavior. + if drive_letter_match: + path = _drive_start_regex.sub( + drive_letter_match.expand("/\\1/").lower(), path + ) - # backslash ==> fwdslash + # Backslash -> fwdslash path = path.replace('\\', '/') return path -def to_windows_path(path): - """Convert (eg) "C:\foo/bin" to "C:\foo\bin" +def to_mixed_path(path): + """Convert (eg) "C:\foo/bin" to "C:/foo/bin" The mixed syntax results from strings in package commands such as "{root}/bin" being interpreted in a windows shell. @@ -44,7 +90,39 @@ def to_windows_path(path): TODO: doesn't take into account escaped forward slashes, which would be weird to have in a path, but is possible. """ - return path.replace('/', '\\') + def uprepl(match): + if match: + return '{}:/'.format(match.group(1).upper()) + + # c:\ and C:\ -> C:/ + drive_letter_match = _drive_start_regex.match(path) + # If converting the drive letter to posix, capitalize the drive + # letter as per cygpath behavior. + if drive_letter_match: + path = _drive_start_regex.sub( + drive_letter_match.expand("\\1:/").upper(), path + ) + + # Fwdslash -> backslash + path = path.replace('\\', '/') + + # ${XYZ};c:/ -> C:/ + if _drive_regex_mixed.match(path): + path = _drive_regex_mixed.sub(uprepl, path) + + return path + + +def to_windows_path(path): + r"""Convert (eg) "C:\foo/bin" to "C:\foo\bin" + + TODO: doesn't take into account escaped forward slashes, which would be + weird to have in a path, but is possible. + """ + # Fwdslash -> backslash + path = path.replace('/', '\\') + + return path def get_syspaths_from_registry(): diff --git a/src/rezplugins/shell/cmd.py b/src/rezplugins/shell/cmd.py index c3afdcc01..a842bb57a 100644 --- a/src/rezplugins/shell/cmd.py +++ b/src/rezplugins/shell/cmd.py @@ -12,7 +12,7 @@ from rez.utils.execution import Popen from rez.utils.platform_ import platform_ from rez.vendor.six import six -from ._utils.windows import to_windows_path, get_syspaths_from_registry +from ._utils.windows import convert_path, get_syspaths_from_registry from functools import partial import os import re @@ -217,14 +217,18 @@ def get_output(self, style=OutputStyle.file): script = '&& '.join(lines) return script - def escape_string(self, value, is_path=False): + def escape_string(self, value, is_path=False, is_shell_path=False): """Escape the <, >, ^, and & special characters reserved by Windows. + ``is_path`` and ``is_shell_path`` are mutually exclusive. + Args: value (str/EscapedString): String or already escaped string. Returns: - str: The value escaped for Windows. + value (str): The value escaped for Windows. + is_path (bool): True if the value is path-like. + is_shell_path (bool): True if the value is a shell-path. """ value = EscapedString.promote(value) @@ -239,13 +243,72 @@ def escape_string(self, value, is_path=False): else: if is_path: txt = self.normalize_paths(txt) + elif is_shell_path: + txt = self.as_shell_path(txt) txt = self._escaper(txt) result += txt return result + def as_path(self, path): + """ + Return the given path as a system path. + Used if the path needs to be reformatted to suit a specific case. + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + return self.normalize_path(path) + + def as_shell_path(self, path): + """ + Return the given path as a shell path. + Used if the shell requires a different pathing structure. + + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + return self.normalize_path(path) + def normalize_path(self, path): - return to_windows_path(path) + """ + Normalize the path to fit the environment. + For example, POSIX paths, Windows path, etc. If no transformation is + necessary, just return the path. + + Args: + path (str): File path. + + Returns: + (str): Normalized file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + converted_path = convert_path(path, 'windows') + + if path != converted_path: + self._addline( + 'REM Path converted: {} -> {}'.format( + path, converted_path + ) + ) + + return converted_path def _saferefenv(self, key): pass @@ -254,8 +317,13 @@ def shebang(self): pass def setenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) - self._addline('set %s=%s' % (key, value)) + new_value = self.escape_string( + value, + is_path=self._is_pathed_key(key), + is_shell_path=self._is_shell_pathed_key(key), + ) + + self._addline('set %s=%s' % (key, new_value)) def unsetenv(self, key): self._addline("set %s=" % key) diff --git a/src/rezplugins/shell/csh.py b/src/rezplugins/shell/csh.py index ab9fbe95a..e7f8efc3b 100644 --- a/src/rezplugins/shell/csh.py +++ b/src/rezplugins/shell/csh.py @@ -14,6 +14,7 @@ from rez.util import shlex_join from rez.utils.execution import Popen from rez.utils.platform_ import platform_ +from rez.utils.logging_ import print_debug from rez.shells import UnixShell from rez.rex import EscapedString @@ -98,7 +99,7 @@ def get_startup_sequence(cls, rcfile, norc, stdin, command): source_bind_files=(not norc) ) - def escape_string(self, value, is_path=False): + def escape_string(self, value, is_path=False, is_shell_path=False): value = EscapedString.promote(value) value = value.expanduser() result = '' @@ -110,7 +111,9 @@ def escape_string(self, value, is_path=False): txt = "'%s'" % txt else: if is_path: - txt = self.normalize_paths(txt) + txt = self.as_path(txt) + elif is_shell_path: + txt = self.as_shell_path(txt) txt = txt.replace('"', '"\\""') txt = txt.replace('!', '\\!') @@ -157,8 +160,18 @@ def _saferefenv(self, key): self._addline("if (!($?%s)) setenv %s" % (key, key)) def setenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) - self._addline('setenv %s %s' % (key, value)) + is_path = self._is_pathed_key(key) or self._is_shell_pathed_key(key) + new_value = self.escape_string(value, is_path=self._is_pathed_key(key)) + + if is_path and value != new_value: + print_debug( + 'Path changed: {} -> {}'.format(value, new_value) + ) + self._addline( + '# Path value changed: {} -> {}'.format(value, new_value) + ) + + self._addline('setenv %s %s' % (key, new_value)) def unsetenv(self, key): self._addline("unsetenv %s" % key) diff --git a/src/rezplugins/shell/gitbash.py b/src/rezplugins/shell/gitbash.py index 8d0392633..9fe6d39c0 100644 --- a/src/rezplugins/shell/gitbash.py +++ b/src/rezplugins/shell/gitbash.py @@ -2,9 +2,7 @@ # Copyright Contributors to the Rez Project -""" -Git Bash (for Windows) shell -""" +"""Git Bash (for Windows) shell.""" import os import re import os.path @@ -16,20 +14,19 @@ from rez.utils.logging_ import print_warning from rez.util import dedup -if platform_.name == "windows": - from ._utils.windows import get_syspaths_from_registry, to_posix_path +if platform_.name == 'windows': + from ._utils.windows import get_syspaths_from_registry, convert_path class GitBash(Bash): - """Git Bash shell plugin. - """ + """Git Bash shell plugin.""" pathsep = ':' - _drive_regex = re.compile(r"([A-Za-z]):\\") + _drive_regex = re.compile(r'([A-Za-z]):\\') @classmethod def name(cls): - return "gitbash" + return 'gitbash' @classmethod def executable_name(cls): @@ -47,6 +44,8 @@ def find_executable(cls, name, check_syspaths=False): "plugins.shell.gitbash.executable_fullpath." ) + exepath = exepath.replace('\\', '\\\\') + return exepath @classmethod @@ -73,7 +72,7 @@ def get_syspaths(cls): out_, _ = p.communicate() if p.returncode == 0: lines = out_.split('\n') - line = [x for x in lines if "__PATHS_" in x.split()][0] + line = [x for x in lines if '__PATHS_' in x.split()][0] # note that we're on windows, but pathsep in bash is ':' paths = line.strip().split()[-1].split(':') else: @@ -88,8 +87,63 @@ def get_syspaths(cls): cls.syspaths = paths return cls.syspaths + def as_path(self, path): + """Return the given path as a system path. + Used if the path needs to be reformatted to suit a specific case. + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + return path + + def as_shell_path(self, path): + """Return the given path as a shell path. + Used if the shell requires a different pathing structure. + + Args: + path (str): File path. + + Returns: + (str): Transformed file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + converted_path = convert_path(path, mode='mixed', force_fwdslash=True) + if path != converted_path: + self._addline( + '# Path converted: {} -> {}'.format(path, converted_path) + ) + return converted_path + def normalize_path(self, path): - return to_posix_path(path) + """Normalize the path to fit the environment. + For example, POSIX paths, Windows path, etc. If no transformation is + necessary, just return the path. + + Args: + path (str): File path. + + Returns: + (str): Normalized file path. + """ + # Prevent path conversion if normalization is disabled in the config. + if config.disable_normalization: + return path + + converted_path = convert_path(path, mode='unix', force_fwdslash=True) + if path != converted_path: + self._addline( + '# Path converted: {} -> {}'.format(path, converted_path) + ) + return converted_path def normalize_paths(self, value): """ @@ -102,13 +156,18 @@ def normalize_paths(self, value): normalize_path() still does drive-colon replace also - it needs to behave correctly if passed a string like C:\foo. """ + def lowrepl(match): + if match: + return '/{}/'.format(match.group(1).lower()) # C:\ ==> /c/ - value2 = self._drive_regex.sub("/\\1/", value) + value2 = self._drive_regex.sub(lowrepl, value).replace('\\', '/') + return value2 - return super(GitBash, self).normalize_paths(value2) + def shebang(self): + self._addline('#! /usr/bin/env bash') def register_plugin(): - if platform_.name == "windows": + if platform_.name == 'windows': return GitBash diff --git a/src/rezplugins/shell/sh.py b/src/rezplugins/shell/sh.py index 7fbade028..3267acf9f 100644 --- a/src/rezplugins/shell/sh.py +++ b/src/rezplugins/shell/sh.py @@ -12,6 +12,7 @@ from rez.config import config from rez.utils.execution import Popen from rez.utils.platform_ import platform_ +from rez.utils.logging_ import print_debug from rez.shells import UnixShell from rez.rex import EscapedString @@ -104,8 +105,25 @@ def _bind_interactive_rez(self): self._addline(cmd % r"\[\e[1m\]$REZ_ENV_PROMPT\[\e[0m\]") def setenv(self, key, value): - value = self.escape_string(value, is_path=self._is_pathed_key(key)) - self._addline('export %s=%s' % (key, value)) + is_implicit = key == 'REZ_USED_IMPLICIT_PACKAGES' + is_path = self._is_pathed_key(key) or self._is_shell_pathed_key(key) + + new_value = self.escape_string( + value, + is_path=self._is_pathed_key(key), + is_shell_path=self._is_shell_pathed_key(key), + is_implicit=is_implicit, + ) + + if is_path and value != new_value: + print_debug( + 'Path value changed: {} -> {}'.format(value, new_value) + ) + self._addline( + '# Path value changed: {} -> {}'.format(value, new_value) + ) + + self._addline('export %s=%s' % (key, new_value)) def unsetenv(self, key): self._addline("unset %s" % key) @@ -119,9 +137,13 @@ def source(self, value): value = self.escape_string(value) self._addline('. %s' % value) - def escape_string(self, value, is_path=False): + def escape_string( + self, value, is_path=False, is_shell_path=False, is_implicit=False + ): value = EscapedString.promote(value) - value = value.expanduser() + if not is_implicit: + value = value.expanduser() + result = '' for is_literal, txt in value.strings: @@ -130,13 +152,16 @@ def escape_string(self, value, is_path=False): if not txt.startswith("'"): txt = "'%s'" % txt else: - if is_path: + if is_shell_path: + txt = self.as_shell_path(txt) + elif is_path: txt = self.normalize_paths(txt) txt = txt.replace('\\', '\\\\') txt = txt.replace('"', '\\"') txt = '"%s"' % txt result += txt + return result def _saferefenv(self, key):