diff --git a/install.py b/install.py index 73aa1f1fb..bdf588623 100644 --- a/install.py +++ b/install.py @@ -35,8 +35,8 @@ # though rez is not yet built. # from rez.utils._version import _rez_version # noqa: E402 +from rez.utils.which import which # noqa: E402 from rez.cli._entry_points import get_specifications # noqa: E402 -from rez.backport.shutilwhich import which # noqa: E402 from rez.vendor.distlib.scripts import ScriptMaker # noqa: E402 # switch to builtin venv in python 3.7+ diff --git a/src/rez/backport/shutilwhich.py b/src/rez/backport/shutilwhich.py deleted file mode 100644 index 0241aad5f..000000000 --- a/src/rez/backport/shutilwhich.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from rez.vendor.whichcraft import whichcraft - - -def which(cmd, mode=os.F_OK | os.X_OK, path=None, env=None): - return whichcraft.which(cmd, mode, path, env) diff --git a/src/rez/resolved_context.py b/src/rez/resolved_context.py index 440602bde..a4ae30e48 100644 --- a/src/rez/resolved_context.py +++ b/src/rez/resolved_context.py @@ -31,7 +31,7 @@ from rez.utils.filesystem import TempDirs, is_subdirectory, canonical_path from rez.utils.memcached import pool_memcached_connections from rez.utils.logging_ import print_error, print_warning -from rez.backport.shutilwhich import which +from rez.utils.which import which from rez.rex import RexExecutor, Python, OutputStyle from rez.rex_bindings import VersionBinding, VariantBinding, \ VariantsBinding, RequirementsBinding, EphemeralsBinding, intersects diff --git a/src/rez/shells.py b/src/rez/shells.py index 3e2f19b34..b710c8ab0 100644 --- a/src/rez/shells.py +++ b/src/rez/shells.py @@ -18,7 +18,7 @@ """ from rez.rex import RexExecutor, ActionInterpreter, OutputStyle from rez.util import shlex_join, is_non_string_iterable -from rez.backport.shutilwhich import which +from rez.utils.which import which from rez.utils.logging_ import print_warning from rez.utils.execution import Popen from rez.system import system diff --git a/src/rez/status.py b/src/rez/status.py index d2d0ec229..b421f5b20 100644 --- a/src/rez/status.py +++ b/src/rez/status.py @@ -27,7 +27,7 @@ from rez.wrapper import Wrapper from rez.utils.colorize import warning, critical, Printer from rez.utils.formatting import print_colored_columns, PackageRequest -from rez.backport.shutilwhich import which +from rez.utils.which import which class Status(object): diff --git a/src/rez/util.py b/src/rez/util.py index 685ab1ce5..a3467e32b 100644 --- a/src/rez/util.py +++ b/src/rez/util.py @@ -89,7 +89,8 @@ def escape_word(s): # returns path to first program in the list to be successfully found def which(*programs, **shutilwhich_kwargs): - from rez.backport.shutilwhich import which as which_ + from rez.utils.which import which as which_ + for prog in programs: path = which_(prog, **shutilwhich_kwargs) if path: diff --git a/src/rez/utils/which.py b/src/rez/utils/which.py new file mode 100644 index 000000000..621676805 --- /dev/null +++ b/src/rez/utils/which.py @@ -0,0 +1,85 @@ +import os +import sys + + +_default_pathext = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC' + + +def which(cmd, mode=os.F_OK | os.X_OK, path=None, env=None): + """A replacement for shutil.which. + + Things we do that shutil.which does not: + + * Support specifying `env` + * Take into account '%systemroot%' possible presence in `path` (windows) + * Take into account symlinks to executables (windows) + """ + iswin = (sys.platform == "win32") + pathext = [] + if env is None: + env = os.environ + + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + # + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script. Note that `path` is ignored in this case. + # + dirname, filename = os.path.split(cmd) + if dirname: + path = dirname + cmd = filename + + if path is None: + path = env.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if iswin: + # The current directory takes precedence on Windows + if not dirname and os.curdir not in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows + pathext = env.get("PATHEXT", _default_pathext).split(os.pathsep) + pathext = [x.lower() for x in pathext] + + # iterate over paths + seen = set() + for dir_ in path: + normdir = os.path.normcase(dir_) + + # On windows the system paths might contain %systemroot% + normdir = os.path.expandvars(normdir) + + if normdir in seen: + continue + seen.add(normdir) + + # search for matching cmd + if iswin: + # Account for cmd possibly being a symlink. A symlink can be an + # executable on windows without an extension. If it is, see if its + # target's extension matches any of the expected path extensions. + # + realfile = os.path.realpath(os.path.join(normdir, cmd)).lower() + if any(realfile.endswith(x) for x in pathext): + files = [cmd] + else: + files = [(cmd + ext) for ext in pathext] + else: + files = [cmd] + + for thefile in files: + name = os.path.join(normdir, thefile) + if _access_check(name, mode): + return name + + return None diff --git a/src/rez/vendor/README.md b/src/rez/vendor/README.md index 0078ec8ea..d244475ab 100644 --- a/src/rez/vendor/README.md +++ b/src/rez/vendor/README.md @@ -236,18 +236,6 @@ Updated (July 2019) to coincide with packaging lib addition that depends on. Also now required to support py2/3 interoperability. - -