Matt Clay 6 days ago committed by GitHub
commit fe2497f9af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,8 +4,8 @@
set -eux
# The third PATH entry is the injected bin directory created by ansible-test.
bin_dir="$(python -c 'import os; print(os.environ["PATH"].split(":")[2])')"
# The PATH entry ending in "-bin" is the injected bin directory created by ansible-test.
bin_dir="$(python -c 'import os; print([path for path in os.environ["PATH"].split(":") if path.endswith("-bin")][0])')"
while IFS= read -r name
do

@ -25,6 +25,8 @@ if ! command -V pwsh; then
exit 0
fi
pwsh --version
# Use a PowerShell-only collection to verify that validate-modules does not load the collection loader multiple times.
ansible-test sanity --test validate-modules --color --truncate 0 "${@}"

@ -0,0 +1,3 @@
context/target
gather_facts/no
shippable/posix/group2

@ -0,0 +1,8 @@
- name: Get PowerShell version
command: pwsh --version
register: powershell_version
failed_when: false
- name: Show PowerShell Version
debug:
msg: "{{ powershell_version.stdout }}"

@ -1,6 +1,6 @@
base image=quay.io/ansible/base-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13
default image=quay.io/ansible/default-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13 context=collection
default image=quay.io/ansible/ansible-core-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13 context=ansible-core
base image=quay.io/ansible/base-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13 powershell=7.5
default image=quay.io/ansible/default-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13 powershell=7.5 context=collection
default image=quay.io/ansible/ansible-core-test-container:v2.21-0 python=3.14,3.9,3.10,3.11,3.12,3.13 powershell=7.5 context=ansible-core
alpine322 image=quay.io/ansible/alpine-test-container:3.22-v2.20-1 python=3.12 cgroup=none audit=none
fedora42 image=quay.io/ansible/fedora-test-container:42-v2.20-1 python=3.13 cgroup=v2-only
ubuntu2204 image=quay.io/ansible/ubuntu-test-container:22.04-v2.20-1 python=3.10

@ -30,7 +30,7 @@ from .util_common import (
create_temp_dir,
ResultType,
intercept_python,
get_injector_path,
get_python_injector_path,
)
from .config import (
@ -122,7 +122,7 @@ def ansible_environment(args: CommonConfig, color: bool = True, ansible_config:
# it only requires the injector for code coverage
# the correct python interpreter is already selected using the sys.executable used to invoke ansible
ansible.update(
_ANSIBLE_CONNECTION_PATH=os.path.join(get_injector_path(), 'ansible_connection_cli_stub.py'),
_ANSIBLE_CONNECTION_PATH=os.path.join(get_python_injector_path(), 'ansible_connection_cli_stub.py'),
)
if isinstance(args, PosixIntegrationConfig):

@ -43,6 +43,7 @@ from ..argparsing.parsers import (
from .value_parsers import (
PythonParser,
PowerShellParser,
)
from .helpers import (
@ -61,6 +62,7 @@ class OriginKeyValueParser(KeyValueParser):
return dict(
python=PythonParser(versions=versions, allow_venv=True, allow_default=True),
powershell=PowerShellParser(),
)
def document(self, state: DocumentationState) -> t.Optional[str]:
@ -87,6 +89,7 @@ class ControllerKeyValueParser(KeyValueParser):
return dict(
python=PythonParser(versions=versions, allow_venv=allow_venv, allow_default=allow_default),
powershell=PowerShellParser(),
)
def document(self, state: DocumentationState) -> t.Optional[str]:
@ -113,6 +116,7 @@ class DockerKeyValueParser(KeyValueParser):
"""Return a dictionary of key names and value parsers."""
return dict(
python=PythonParser(versions=self.versions, allow_venv=False, allow_default=self.allow_default),
powershell=PowerShellParser(),
seccomp=ChoicesParser(SECCOMP_CHOICES),
cgroup=EnumValueChoicesParser(CGroupVersion),
audit=EnumValueChoicesParser(AuditMode),
@ -153,6 +157,7 @@ class PosixRemoteKeyValueParser(KeyValueParser):
provider=ChoicesParser(REMOTE_PROVIDERS),
arch=ChoicesParser(REMOTE_ARCHITECTURES),
python=PythonParser(versions=self.versions, allow_venv=False, allow_default=self.allow_default),
powershell=PowerShellParser(),
)
def document(self, state: DocumentationState) -> t.Optional[str]:
@ -228,6 +233,7 @@ class PosixSshKeyValueParser(KeyValueParser):
"""Return a dictionary of key names and value parsers."""
return dict(
python=PythonParser(versions=list(SUPPORTED_PYTHON_VERSIONS), allow_venv=False, allow_default=False),
powershell=PowerShellParser(),
)
def document(self, state: DocumentationState) -> t.Optional[str]:

@ -5,10 +5,15 @@ from __future__ import annotations
import collections.abc as c
import typing as t
from ...constants import (
SUPPORTED_POWERSHELL_VERSIONS,
)
from ...host_configs import (
NativePythonConfig,
PythonConfig,
VirtualPythonConfig,
PowerShellConfig,
)
from ..argparsing.parsers import (
@ -135,6 +140,20 @@ class PythonParser(Parser):
return docs
class PowerShellParser(ChoicesParser):
"""
Composite argument parser for PowerShell versions.
"""
def __init__(self) -> None:
super().__init__(SUPPORTED_POWERSHELL_VERSIONS)
def parse(self, state: ParserState) -> t.Any:
return PowerShellConfig(
version=super().parse(state),
)
class PlatformParser(ChoicesParser):
"""Composite argument parser for "{platform}/{version}" formatted choices."""

@ -21,12 +21,14 @@ from ...util import (
display,
ApplicationError,
raw_command,
common_environment,
)
from ...util_common import (
ResultType,
write_json_file,
write_json_test_results,
get_powershell_injector_env,
)
from ...executor import (
@ -197,10 +199,13 @@ def _command_coverage_combine_powershell(args: CoverageCombineConfig) -> list[st
coverage_files = get_powershell_coverage_files()
def _default_stub_value(source_paths: list[str]) -> dict[str, dict[int, int]]:
env = common_environment()
env.update(get_powershell_injector_env(args.controller_powershell, env))
cmd = ['pwsh', os.path.join(ANSIBLE_TEST_TOOLS_ROOT, 'coverage_stub.ps1')]
cmd.extend(source_paths)
stubs = json.loads(raw_command(cmd, capture=True)[0])
stubs = json.loads(raw_command(cmd, env=env, capture=True)[0])
return dict((d['Path'], dict((line, 0) for line in d['Lines'])) for d in stubs)

@ -69,6 +69,7 @@ from ...util_common import (
run_command,
write_json_test_results,
check_pyyaml,
get_powershell_injector_env,
)
from ...coverage_util import (
@ -629,6 +630,7 @@ def command_integration_script(
cmd += ['-e', '@%s' % config_path]
env.update(coverage_manager.get_environment(target.name, target.aliases))
env.update(get_powershell_injector_env(host_state.controller_profile.powershell, env))
cover_python(args, host_state.controller_profile.python, cmd, target.name, env, cwd=cwd, capture=False)
@ -748,6 +750,7 @@ def command_integration_role(
env['ANSIBLE_ROLES_PATH'] = test_env.targets_dir
env.update(coverage_manager.get_environment(target.name, target.aliases))
env.update(get_powershell_injector_env(host_state.controller_profile.powershell, env))
cover_python(args, host_state.controller_profile.python, cmd, target.name, env, cwd=cwd, capture=False)

@ -29,10 +29,12 @@ from ...util import (
SubprocessError,
find_executable,
ANSIBLE_TEST_DATA_ROOT,
common_environment,
)
from ...util_common import (
run_command,
get_powershell_injector_env,
)
from ...config import (
@ -57,6 +59,9 @@ class PslintTest(SanityVersionNeutral):
return [target for target in targets if os.path.splitext(target.path)[1] in ('.ps1', '.psm1', '.psd1')]
def test(self, args: SanityConfig, targets: SanityTargets) -> TestResult:
env = common_environment()
env.update(get_powershell_injector_env(args.controller_powershell, env))
settings = self.load_processor(args)
paths = [target.path for target in targets.include]
@ -75,7 +80,7 @@ class PslintTest(SanityVersionNeutral):
for cmd in cmds:
try:
stdout, stderr = run_command(args, cmd, capture=True)
stdout, stderr = run_command(args, cmd, env=env, capture=True)
status = 0
except SubprocessError as ex:
stdout = ex.stdout

@ -42,6 +42,7 @@ from ...util_common import (
process_scoped_temporary_directory,
run_command,
ResultType,
get_powershell_injector_env,
)
from ...ansible_util import (
@ -122,6 +123,7 @@ class ValidateModulesTest(SanitySingleVersion):
def test(self, args: SanityConfig, targets: SanityTargets, python: PythonConfig) -> TestResult:
env = ansible_environment(args, color=False)
env.update(get_powershell_injector_env(args.controller_powershell, env))
settings = self.load_processor(args)

@ -63,7 +63,8 @@ from ...python_requirements import (
)
from ...util_common import (
get_injector_env,
get_python_injector_env,
get_powershell_injector_env,
)
from ...delegation import (
@ -206,7 +207,8 @@ def get_environment_variables(
if isinstance(con, LocalConnection): # configure the controller environment
env.update(ansible_environment(args))
env.update(get_injector_env(target_profile.python, env))
env.update(get_python_injector_env(target_profile.python, env))
env.update(get_powershell_injector_env(target_profile.powershell, env))
env.update(ANSIBLE_TEST_METADATA_PATH=os.path.abspath(args.metadata_path))
if isinstance(target_profile, DebuggableProfile):

@ -11,6 +11,7 @@ import typing as t
from .constants import (
CONTROLLER_PYTHON_VERSIONS,
SUPPORTED_PYTHON_VERSIONS,
SUPPORTED_POWERSHELL_VERSIONS,
)
from .util import (
@ -71,16 +72,32 @@ class PosixCompletionConfig(CompletionConfig, metaclass=abc.ABCMeta):
def supported_pythons(self) -> list[str]:
"""Return a list of the supported Python versions."""
@property
@abc.abstractmethod
def supported_powershells(self) -> list[str]:
"""Return a list of the supported PowerShell versions."""
@abc.abstractmethod
def get_python_path(self, version: str) -> str:
"""Return the path of the requested Python version."""
@abc.abstractmethod
def get_powershell_path(self, version: str | None) -> str | None:
"""
Return the path of the requested PowerShell version, or None if it is not found.
If no version is specified, look for the default unversioned 'pwsh' interpreter.
"""
def get_default_python(self, controller: bool) -> str:
"""Return the default Python version for a controller or target as specified."""
context_pythons = CONTROLLER_PYTHON_VERSIONS if controller else SUPPORTED_PYTHON_VERSIONS
version = [python for python in self.supported_pythons if python in context_pythons][0]
return version
def get_default_powershell(self) -> str | None:
"""Return the default PowerShell version, or None if there is no default."""
return None
@property
def controller_supported(self) -> bool:
"""True if at least one Python version is provided which supports the controller, otherwise False."""
@ -106,6 +123,28 @@ class PythonCompletionConfig(PosixCompletionConfig, metaclass=abc.ABCMeta):
return os.path.join(self.python_dir, f'python{version}')
@dataclasses.dataclass(frozen=True)
class PowerShellCompletionConfig(PosixCompletionConfig, metaclass=abc.ABCMeta):
"""Base class for completion configuration of PowerShell environments."""
powershell: str = ''
powershell_dir: str = '/usr/bin'
@property
def supported_powershells(self) -> list[str]:
"""Return a list of the supported PowerShell versions."""
versions = self.powershell.split(',') if self.powershell else []
versions = [version for version in versions if version in SUPPORTED_POWERSHELL_VERSIONS]
return versions
def get_powershell_path(self, version: str | None) -> str:
"""
Return the path of the requested PowerShell version.
If no version is specified, look for the default unversioned 'pwsh' interpreter.
"""
return os.path.join(self.powershell_dir, f'pwsh{version or ""}')
@dataclasses.dataclass(frozen=True)
class RemoteCompletionConfig(CompletionConfig):
"""Base class for completion configuration of remote environments provisioned through Ansible Core CI."""
@ -150,7 +189,7 @@ class InventoryCompletionConfig(CompletionConfig):
@dataclasses.dataclass(frozen=True)
class PosixSshCompletionConfig(PythonCompletionConfig):
class PosixSshCompletionConfig(PythonCompletionConfig, PowerShellCompletionConfig):
"""Configuration for a POSIX host reachable over SSH."""
def __init__(self, user: str, host: str) -> None:
@ -166,7 +205,7 @@ class PosixSshCompletionConfig(PythonCompletionConfig):
@dataclasses.dataclass(frozen=True)
class DockerCompletionConfig(PythonCompletionConfig):
class DockerCompletionConfig(PythonCompletionConfig, PowerShellCompletionConfig):
"""Configuration for Docker containers."""
image: str = ''
@ -196,6 +235,10 @@ class DockerCompletionConfig(PythonCompletionConfig):
except ValueError:
raise ValueError(f'Docker completion entry "{self.name}" has an invalid value "{self.cgroup}" for the "cgroup" setting.') from None
def get_default_powershell(self) -> str | None:
"""Return the default PowerShell version, or None if there is no default."""
return next(iter(self.supported_powershells), None)
def __post_init__(self):
if not self.image:
raise Exception(f'Docker completion entry "{self.name}" must provide an "image" setting.')
@ -222,12 +265,16 @@ class NetworkRemoteCompletionConfig(RemoteCompletionConfig):
@dataclasses.dataclass(frozen=True)
class PosixRemoteCompletionConfig(RemoteCompletionConfig, PythonCompletionConfig):
class PosixRemoteCompletionConfig(RemoteCompletionConfig, PythonCompletionConfig, PowerShellCompletionConfig):
"""Configuration for remote POSIX platforms."""
become: t.Optional[str] = None
placeholder: bool = False
def get_default_powershell(self) -> str | None:
"""Return the default PowerShell version, or None if there is no default."""
return next(iter(self.supported_powershells), None)
def __post_init__(self):
if not self.placeholder:
super().__post_init__()

@ -36,6 +36,7 @@ from .host_configs import (
OriginConfig,
PythonConfig,
VirtualPythonConfig,
PowerShellConfig,
)
@ -91,6 +92,14 @@ class EnvironmentConfig(CommonConfig):
Only available after delegation has been performed or skipped (if delegation is not required).
"""
# Set by check_controller_powershell once HostState has been created by prepare_profiles.
# This is here for convenience, to avoid needing to pass HostState to some functions which already have access to EnvironmentConfig.
self.controller_powershell: PowerShellConfig | None = None
"""
The PowerShell interpreter used by the controller.
Only available after delegation has been performed or skipped (if delegation is not required).
"""
if self.host_path:
self.delegate = False
else:

@ -19,6 +19,13 @@ TIMEOUT_PATH = '.ansible-test-timeout.json'
CONTROLLER_MIN_PYTHON_VERSION = CONTROLLER_PYTHON_VERSIONS[0]
SUPPORTED_PYTHON_VERSIONS = REMOTE_ONLY_PYTHON_VERSIONS + CONTROLLER_PYTHON_VERSIONS
SUPPORTED_POWERSHELL_VERSIONS = [
'7.6',
'7.5',
'7.4',
]
"""PowerShell versions supported by ansible-test, listed in order of preference."""
REMOTE_PROVIDERS = [
'default',
'aws',

@ -12,6 +12,7 @@ import typing as t
from .constants import (
SUPPORTED_PYTHON_VERSIONS,
SUPPORTED_POWERSHELL_VERSIONS,
)
from .io import (
@ -43,6 +44,7 @@ from .util import (
str_to_version,
version_to_str,
Architecture,
find_executable,
)
@ -66,6 +68,21 @@ class OriginCompletionConfig(PosixCompletionConfig):
version = find_python(version)
return version
@property
def supported_powershells(self) -> list[str]:
"""Return a list of the supported PowerShell versions."""
return SUPPORTED_POWERSHELL_VERSIONS
def get_powershell_path(self, version: str | None) -> str | None:
"""
Return the path of the requested PowerShell version, or None if it is not found.
If no version is specified, look for the default unversioned 'pwsh' interpreter.
"""
if not version:
pass # FIXME: when does the origin have no version, and what to do about it?
return find_executable(f'pwsh{version or ""}', required=bool(version)) # FIXME: is this the behavior we want?
@property
def is_default(self) -> bool:
"""True if the completion entry is only used for defaults, otherwise False."""
@ -179,11 +196,43 @@ class VirtualPythonConfig(PythonConfig):
return True
@dataclasses.dataclass
class PowerShellConfig:
"""Configuration for PowerShell."""
version: str | None = None
path: str | None = None
@property
def tuple(self) -> tuple[int, ...]:
"""Return the PowerShell version as a tuple."""
return str_to_version(self.version)
@property
def major_version(self) -> int:
"""Return the PowerShell major version."""
return self.tuple[0]
def apply_defaults(self, context: HostContext, defaults: PosixCompletionConfig) -> None:
"""Apply default settings."""
if self.version in (None, 'default'):
self.version = defaults.get_default_powershell()
if self.path:
if self.path.endswith('/'):
self.path = os.path.join(self.path, f'pwsh{self.version or ""}') # FIXME: is the version adding and/or fallback behavior what we want?
# FUTURE: If the host is origin, the pwsh path could be validated here.
else:
self.path = defaults.get_powershell_path(self.version)
@dataclasses.dataclass
class PosixConfig(HostConfig, metaclass=abc.ABCMeta):
"""Base class for POSIX host configuration."""
python: t.Optional[PythonConfig] = None
powershell: PowerShellConfig | None = None
@property
@abc.abstractmethod
@ -203,6 +252,9 @@ class PosixConfig(HostConfig, metaclass=abc.ABCMeta):
self.python = self.python or NativePythonConfig()
self.python.apply_defaults(context, defaults)
self.powershell = self.powershell or PowerShellConfig()
self.powershell.apply_defaults(context, defaults)
@dataclasses.dataclass
class ControllerHostConfig(PosixConfig, metaclass=abc.ABCMeta):
@ -492,6 +544,11 @@ class ControllerConfig(PosixConfig):
# The user did not specify a target Python and supported Pythons are unknown, so use the controller Python specified by the user instead.
self.python = context.controller_config.python
if not self.powershell and not defaults.supported_powershells:
# FIXME: is this logic correct?
# The user did not specify a target PowerShell and supported versions are unknown, so use the controller version specified by the user instead.
self.powershell = context.controller_config.powershell
super().apply_defaults(context, defaults)
@property

@ -40,6 +40,7 @@ from .host_configs import (
VirtualPythonConfig,
WindowsInventoryConfig,
WindowsRemoteConfig,
PowerShellConfig,
)
from .core_ci import (
@ -474,6 +475,20 @@ class PosixProfile[TPosixConfig: PosixConfig](HostProfile[TPosixConfig], metacla
return python
@property
def powershell(self) -> PowerShellConfig:
"""
The PowerShell to use for this profile.
"""
powershell = self.state.get('powershell')
if not powershell:
powershell = self.config.powershell
self.state['powershell'] = powershell
return powershell
class ControllerHostProfile[T: ControllerHostConfig](PosixProfile[T], DebuggableProfile[T], metaclass=abc.ABCMeta):
"""Base class for profiles usable as a controller."""

@ -147,6 +147,7 @@ def prepare_profiles[TEnvironmentConfig: EnvironmentConfig](
if not args.delegate:
check_controller_python(args, host_state)
check_controller_powershell(args, host_state)
if requirements:
requirements(host_state.controller_profile)
@ -184,6 +185,13 @@ def check_controller_python(args: EnvironmentConfig, host_state: HostState) -> N
args.controller_python = controller_python
def check_controller_powershell(args: EnvironmentConfig, host_state: HostState) -> None:
"""Check the running environment to make sure it is what we expected."""
controller_powershell = host_state.controller_profile.powershell
args.controller_powershell = controller_powershell
def cleanup_profiles(host_state: HostState) -> None:
"""Cleanup provisioned hosts when exiting."""
for profile in host_state.profiles:

@ -58,6 +58,7 @@ from .constants import (
)
PYTHON_PATHS: dict[str, str] = {}
POWERSHELL_PATHS: dict[str, str] = {}
COVERAGE_CONFIG_NAME = 'coveragerc'

@ -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],

Loading…
Cancel
Save