# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project
"""
Utilities related to process/script execution.
"""
from rez.vendor.six import six
from rez.utils.yaml import dump_yaml
from rez.vendor.enum import Enum
from contextlib import contextmanager
import subprocess
import sys
import stat
import os
import io
[docs]@contextmanager
def add_sys_paths(paths):
"""Add to sys.path, and revert on scope exit.
"""
original_syspath = sys.path[:]
sys.path.extend(paths)
try:
yield
finally:
sys.path = original_syspath
if six.PY2:
class _PopenBase(subprocess.Popen):
def __enter__(self):
return self
def __exit__(self, exc_type, value, traceback):
self.wait()
else: # py3
_PopenBase = subprocess.Popen
[docs]class Popen(_PopenBase):
"""subprocess.Popen wrapper.
Allows for Popen to be used as a context in both py2 and py3.
"""
def __init__(self, args, **kwargs):
# Avoids python bug described here: https://bugs.python.org/issue3905.
# This can arise when apps (maya) install a non-standard stdin handler.
#
# In newer version of maya and katana, the sys.stdin object can also
# become replaced by an object with no 'fileno' attribute, this is also
# taken into account.
#
if "stdin" not in kwargs:
try:
file_no = sys.stdin.fileno()
# https://github.com/nerdvegas/rez/pull/966
except (AttributeError, io.UnsupportedOperation):
file_no = None
if file_no is None and sys.__stdin__ is not None:
file_no = sys.__stdin__.fileno()
if file_no not in (0, 1, 2):
kwargs["stdin"] = subprocess.PIPE
# Add support for the new py3 "text" arg, which is equivalent to
# "universal_newlines".
# https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
#
text = kwargs.pop("text", None)
universal_newlines = kwargs.pop("universal_newlines", None)
if text or universal_newlines:
kwargs["universal_newlines"] = True
# fixes py3/cmd.exe UnicodeDecodeError() with some characters.
# UnicodeDecodeError: 'charmap' codec can't decode byte
# 0x8d in position 1023172: character maps to <undefined>
#
# NOTE: currently no solution for `python3+<3.6`
#
if sys.version_info[:2] >= (3, 6) and "encoding" not in kwargs:
kwargs["encoding"] = "utf-8"
super(Popen, self).__init__(args, **kwargs)
[docs]class ExecutableScriptMode(Enum):
"""
Which scripts to create with util.create_executable_script.
"""
# Start with 1 to not collide with None checks
# Requested script only. Usually extension-less.
single = 1
# Create .py script that will allow launching scripts on
# windows without extension, but may require extension on
# other systems.
py = 2
# Will create py script on windows and requested on
# other platforms
platform_specific = 3
# Creates the requested script and an .py script so that scripts
# can be launched without extension from windows and other
# systems.
both = 4
# TODO: Maybe also allow distlib.ScriptMaker instead of the .py + PATHEXT.
[docs]def create_executable_script(filepath, body, program=None, py_script_mode=None):
"""
Create an executable script. In case a py_script_mode has been set to create
a .py script the shell is expected to have the PATHEXT environment
variable to include ".PY" in order to properly launch the command without
the .py extension.
Args:
filepath (str): File to create.
body (str or callable): Contents of the script. If a callable, its code
is used as the script body.
program (str): Name of program to launch the script. Default is 'python'
py_script_mode(ExecutableScriptMode): What kind of script to create.
Defaults to rezconfig.create_executable_script_mode.
Returns:
List of filepaths of created scripts. This may differ from the supplied
filepath depending on the py_script_mode
"""
from rez.config import config
from rez.utils.platform_ import platform_
program = program or "python"
py_script_mode = py_script_mode or config.create_executable_script_mode
# https://github.com/nerdvegas/rez/pull/968
is_forwarding_script_on_windows = (
program == "_rez_fwd"
and platform_.name == "windows"
and filepath.lower().endswith(".cmd")
)
if callable(body):
from rez.utils.sourcecode import SourceCode
code = SourceCode(func=body)
body = code.source
if not body.endswith('\n'):
body += '\n'
# Windows does not support shebang, but it will run with
# default python, or in case of later python versions 'py' that should
# try to use sensible python interpreters depending on the shebang line.
# Compare PEP-397.
# In order for execution to work in windows we need to create a .py
# file and set the PATHEXT to include .py (as done by the shell plugins)
# So depending on the py_script_mode we might need to create more then
# one script
script_filepaths = [filepath]
if program == "python":
script_filepaths = _get_python_script_files(filepath, py_script_mode,
platform_.name)
for current_filepath in script_filepaths:
with open(current_filepath, 'w') as f:
# TODO: make cross platform
if is_forwarding_script_on_windows:
# following lines of batch script will be stripped
# before yaml.load
f.write("@echo off\n")
f.write("%s.exe %%~dpnx0 %%*\n" % program)
f.write("goto :eof\n") # skip YAML body
f.write(":: YAML\n") # comment for human
else:
f.write("#!/usr/bin/env %s\n" % program)
f.write(body)
# TODO: Although Windows supports os.chmod you can only set the readonly
# flag. Setting the file readonly breaks the unit tests that expect to
# clean up the files once the test has run. Temporarily we don't bother
# setting the permissions, but this will need to change.
if os.name == "posix":
os.chmod(
current_filepath,
stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IXUSR
| stat.S_IXGRP | stat.S_IXOTH
)
return script_filepaths
def _get_python_script_files(filepath, py_script_mode, platform):
"""
Evaluates the py_script_mode for the requested filepath on the given
platform.
Args:
filepath: requested filepath
py_script_mode (ExecutableScriptMode):
platform (str): Platform to evaluate the script files for
Returns:
list of str: filepaths of scripts to create based on inputs
"""
script_filepaths = []
base_filepath, extension = os.path.splitext(filepath)
has_py_ext = extension == ".py"
is_windows = platform == "windows"
if (
py_script_mode == ExecutableScriptMode.single
or py_script_mode == ExecutableScriptMode.both
or (py_script_mode == ExecutableScriptMode.py and has_py_ext)
or (py_script_mode == ExecutableScriptMode.platform_specific and not is_windows)
or (py_script_mode == ExecutableScriptMode.platform_specific and is_windows and has_py_ext)
):
script_filepaths.append(filepath)
if (
not has_py_ext
and (
py_script_mode == ExecutableScriptMode.both
or py_script_mode == ExecutableScriptMode.py
or (py_script_mode == ExecutableScriptMode.platform_specific and is_windows)
)
):
script_filepaths.append(base_filepath + ".py")
return script_filepaths
[docs]def create_forwarding_script(filepath, module, func_name, *nargs, **kwargs):
"""Create a 'forwarding' script.
A forwarding script is one that executes some arbitrary Rez function. This
is used internally by Rez to dynamically create a script that uses Rez,
even though the parent environment may not be configured to do so.
"""
from rez.utils.platform_ import platform_
if platform_.name == "windows" and \
os.path.splitext(filepath)[-1].lower() != ".cmd":
filepath += ".cmd"
doc = dict(
module=module,
func_name=func_name)
if nargs:
doc["nargs"] = nargs
if kwargs:
doc["kwargs"] = kwargs
body = dump_yaml(doc)
create_executable_script(filepath, body, "_rez_fwd")