|
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import collections.abc as c
|
|
|
|
|
import contextlib
|
|
|
|
|
import functools
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
@ -31,6 +32,7 @@ from .util import (
|
|
|
|
|
MODE_FILE,
|
|
|
|
|
OutputStream,
|
|
|
|
|
PYTHON_PATHS,
|
|
|
|
|
POWERSHELL_PATHS,
|
|
|
|
|
raw_command,
|
|
|
|
|
ANSIBLE_TEST_DATA_ROOT,
|
|
|
|
|
ANSIBLE_TEST_TARGET_ROOT,
|
|
|
|
|
@ -58,7 +60,7 @@ from .provider.layout import (
|
|
|
|
|
|
|
|
|
|
from .host_configs import (
|
|
|
|
|
PythonConfig,
|
|
|
|
|
VirtualPythonConfig,
|
|
|
|
|
VirtualPythonConfig, PowerShellConfig,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
CHECK_YAML_VERSIONS: dict[str, t.Any] = {}
|
|
|
|
|
@ -297,7 +299,7 @@ def write_text_test_results(category: ResultType, name: str, content: str) -> No
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cache
|
|
|
|
|
def get_injector_path() -> str:
|
|
|
|
|
def get_python_injector_path() -> str:
|
|
|
|
|
"""Return the path to a directory which contains a `python.py` executable and associated injector scripts."""
|
|
|
|
|
injector_path = tempfile.mkdtemp(prefix='ansible-test-', suffix='-injector', dir='/tmp')
|
|
|
|
|
|
|
|
|
|
@ -365,18 +367,28 @@ def set_shebang(script: str, executable: str) -> str:
|
|
|
|
|
|
|
|
|
|
def get_python_path(interpreter: str) -> str:
|
|
|
|
|
"""Return the path to a directory which contains a `python` executable that runs the specified interpreter."""
|
|
|
|
|
python_path = PYTHON_PATHS.get(interpreter)
|
|
|
|
|
return get_injection_wrapper(interpreter, 'python', PYTHON_PATHS)
|
|
|
|
|
|
|
|
|
|
if python_path:
|
|
|
|
|
return python_path
|
|
|
|
|
|
|
|
|
|
prefix = 'python-'
|
|
|
|
|
def get_powershell_path(interpreter: str) -> str:
|
|
|
|
|
"""Return the path to a directory which contains a `pwsh` executable that runs the specified interpreter."""
|
|
|
|
|
return get_injection_wrapper(interpreter, 'pwsh', POWERSHELL_PATHS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_injection_wrapper(interpreter: str, name: str, cached_paths: dict[str, str]) -> str:
|
|
|
|
|
"""Return the path to a directory which contains the named executable that runs the specified interpreter."""
|
|
|
|
|
injected_path = cached_paths.get(interpreter)
|
|
|
|
|
|
|
|
|
|
if injected_path:
|
|
|
|
|
return injected_path
|
|
|
|
|
|
|
|
|
|
prefix = f'{name}-'
|
|
|
|
|
suffix = '-ansible'
|
|
|
|
|
|
|
|
|
|
root_temp_dir = '/tmp'
|
|
|
|
|
|
|
|
|
|
python_path = tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=root_temp_dir)
|
|
|
|
|
injected_interpreter = os.path.join(python_path, 'python')
|
|
|
|
|
injected_path = tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=root_temp_dir)
|
|
|
|
|
injected_interpreter = os.path.join(injected_path, name)
|
|
|
|
|
|
|
|
|
|
# A symlink is faster than the execv wrapper, but isn't guaranteed to provide the correct result.
|
|
|
|
|
# There are several scenarios known not to work with symlinks:
|
|
|
|
|
@ -390,14 +402,14 @@ def get_python_path(interpreter: str) -> str:
|
|
|
|
|
|
|
|
|
|
create_interpreter_wrapper(interpreter, injected_interpreter)
|
|
|
|
|
|
|
|
|
|
verified_chmod(python_path, MODE_DIRECTORY)
|
|
|
|
|
verified_chmod(injected_path, MODE_DIRECTORY)
|
|
|
|
|
|
|
|
|
|
if not PYTHON_PATHS:
|
|
|
|
|
ExitHandler.register(cleanup_python_paths)
|
|
|
|
|
if not cached_paths:
|
|
|
|
|
ExitHandler.register(functools.partial(cleanup_injector_paths, cached_paths))
|
|
|
|
|
|
|
|
|
|
PYTHON_PATHS[interpreter] = python_path
|
|
|
|
|
cached_paths[interpreter] = injected_path
|
|
|
|
|
|
|
|
|
|
return python_path
|
|
|
|
|
return injected_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_temp_dir(prefix: t.Optional[str] = None, suffix: t.Optional[str] = None, base_dir: t.Optional[str] = None) -> str:
|
|
|
|
|
@ -408,33 +420,33 @@ def create_temp_dir(prefix: t.Optional[str] = None, suffix: t.Optional[str] = No
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_interpreter_wrapper(interpreter: str, injected_interpreter: str) -> None:
|
|
|
|
|
"""Create a wrapper for the given Python interpreter at the specified path."""
|
|
|
|
|
"""Create a wrapper for the given interpreter at the specified path."""
|
|
|
|
|
# sys.executable is used for the shebang to guarantee it is a binary instead of a script
|
|
|
|
|
# injected_interpreter could be a script from the system or our own wrapper created for the --venv option
|
|
|
|
|
shebang_interpreter = sys.executable
|
|
|
|
|
|
|
|
|
|
code = textwrap.dedent("""
|
|
|
|
|
#!%s
|
|
|
|
|
code = textwrap.dedent(f"""
|
|
|
|
|
#!{shebang_interpreter}
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from os import execv
|
|
|
|
|
from sys import argv
|
|
|
|
|
|
|
|
|
|
python = '%s'
|
|
|
|
|
interpreter = {interpreter!r}
|
|
|
|
|
|
|
|
|
|
execv(python, [python] + argv[1:])
|
|
|
|
|
""" % (shebang_interpreter, interpreter)).lstrip()
|
|
|
|
|
execv(interpreter, [interpreter] + argv[1:])
|
|
|
|
|
""").lstrip()
|
|
|
|
|
|
|
|
|
|
write_text_file(injected_interpreter, code)
|
|
|
|
|
|
|
|
|
|
verified_chmod(injected_interpreter, MODE_FILE_EXECUTE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cleanup_python_paths() -> None:
|
|
|
|
|
"""Clean up all temporary python directories."""
|
|
|
|
|
for path in sorted(PYTHON_PATHS.values()):
|
|
|
|
|
display.info('Cleaning up temporary python directory: %s' % path, verbosity=2)
|
|
|
|
|
def cleanup_injector_paths(cached_paths: dict[str, str]) -> None:
|
|
|
|
|
"""Clean up all temporary injector directories."""
|
|
|
|
|
for path in sorted(cached_paths.values()):
|
|
|
|
|
display.info(f'Cleaning up temporary injector directory: {path}', verbosity=2)
|
|
|
|
|
remove_tree(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -454,18 +466,18 @@ def intercept_python(
|
|
|
|
|
Otherwise, a temporary directory will be created to ensure the correct Python can be found in PATH.
|
|
|
|
|
"""
|
|
|
|
|
cmd = list(cmd)
|
|
|
|
|
env = get_injector_env(python, env)
|
|
|
|
|
env = get_python_injector_env(python, env)
|
|
|
|
|
|
|
|
|
|
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd, always=always)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_injector_env(
|
|
|
|
|
def get_python_injector_env(
|
|
|
|
|
python: PythonConfig,
|
|
|
|
|
env: dict[str, str],
|
|
|
|
|
) -> dict[str, str]:
|
|
|
|
|
"""Get the environment variables needed to inject the given Python interpreter into the environment."""
|
|
|
|
|
env = env.copy()
|
|
|
|
|
inject_path = get_injector_path()
|
|
|
|
|
inject_path = get_python_injector_path()
|
|
|
|
|
|
|
|
|
|
# make sure scripts (including injector.py) find the correct Python interpreter
|
|
|
|
|
if isinstance(python, VirtualPythonConfig):
|
|
|
|
|
@ -480,6 +492,25 @@ def get_injector_env(
|
|
|
|
|
return env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_powershell_injector_env(
|
|
|
|
|
powershell: PowerShellConfig | None,
|
|
|
|
|
env: dict[str, str],
|
|
|
|
|
) -> dict[str, str]:
|
|
|
|
|
"""Get the environment variables needed to inject the given PowerShell interpreter into the environment."""
|
|
|
|
|
env = env.copy()
|
|
|
|
|
|
|
|
|
|
if not powershell or not powershell.path or not powershell.version:
|
|
|
|
|
return env # FIXME: how should the absence of pwsh and/or no powershell version specified be handled?
|
|
|
|
|
|
|
|
|
|
powershell_path = get_powershell_path(powershell.path)
|
|
|
|
|
|
|
|
|
|
env['PATH'] = os.path.pathsep.join([powershell_path, env['PATH']])
|
|
|
|
|
env['ANSIBLE_TEST_POWERSHELL_VERSION'] = powershell.version
|
|
|
|
|
env['ANSIBLE_TEST_POWERSHELL_INTERPRETER'] = powershell.path
|
|
|
|
|
|
|
|
|
|
return env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_command(
|
|
|
|
|
args: CommonConfig,
|
|
|
|
|
cmd: c.Iterable[str],
|
|
|
|
|
|