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. - - -whichcraft - -0.6.1 - -BSD 3-Clause - -https://github.com/cookiecutter/whichcraft
-unmodified version added in commit f5b0ea0 - - yaml lib (PyYAML) diff --git a/src/rez/vendor/whichcraft/LICENSE b/src/rez/vendor/whichcraft/LICENSE deleted file mode 100644 index 58b63d2ef..000000000 --- a/src/rez/vendor/whichcraft/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (c) 2015-2016, Daniel Roy Greenfeld -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -* Neither the name of whichcraft nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/rez/vendor/whichcraft/__init__.py b/src/rez/vendor/whichcraft/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/rez/vendor/whichcraft/whichcraft.py b/src/rez/vendor/whichcraft/whichcraft.py deleted file mode 100644 index a50640aaa..000000000 --- a/src/rez/vendor/whichcraft/whichcraft.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- - -__author__ = "Daniel Roy Greenfeld" -__email__ = "pydanny@gmail.com" -__version__ = "0.6.1" - -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): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. - Note: This function was backported from the Python 3 source code. - """ - # 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. - - # NOTE: Modified for rez, original code in commit: - # https://github.com/nerdvegas/rez/commit/f5b0ea07ea2a8d9dae691b0768f63a9239ca7c91 - - 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 - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - - return None - - if env is None: - env = os.environ - - if path is None: - path = env.get("PATH", os.defpath) - if not path: - return None - - path = path.split(os.pathsep) - - if sys.platform == "win32": - # The current directory takes precedence on Windows. - if os.curdir not in path: - path.insert(0, os.curdir) - - # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", _default_pathext).split(os.pathsep) - # See if the given file matches any of the expected path - # extensions. This will allow us to short circuit when given - # "python.exe". If it does match, only test that one, otherwise we - # have to try others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext.lower() for ext in pathext] - else: - # On other platforms you don't have things like PATHEXT to tell you - # what file suffixes are executable, so just pass on cmd as-is. - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if normdir not in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - - # On windows the system paths might have %systemroot% - name = os.path.expandvars(name) - - if _access_check(name, mode): - return name - - return None diff --git a/src/rezplugins/build_system/cmake.py b/src/rezplugins/build_system/cmake.py index 02ab9cbf7..0673f1266 100644 --- a/src/rezplugins/build_system/cmake.py +++ b/src/rezplugins/build_system/cmake.py @@ -26,7 +26,7 @@ from rez.packages import get_developer_package from rez.utils.platform_ import platform_ from rez.config import config -from rez.backport.shutilwhich import which +from rez.utils.which import which from rez.vendor.schema.schema import Or from rez.vendor.six import six from rez.shells import create_shell