"""Shell execution backends for running task commands."""
from __future__ import annotations
import os
import subprocess
from abc import ABC, abstractmethod
from pathlib import Path
from conda.base.constants import on_win
[docs]
class ShellBackend(ABC):
"""Abstract interface for executing shell commands."""
[docs]
@abstractmethod
def run(
self,
cmd: str | list[str],
env: dict[str, str],
cwd: Path,
conda_prefix: Path | None = None,
clean_env: bool = False,
) -> int:
"""Execute *cmd* and return the exit code."""
[docs]
class SubprocessShell(ShellBackend):
"""Default backend using native shell + conda's activation machinery.
When *conda_prefix* is given the command is executed inside an
activated conda environment (mirroring ``conda run``). Otherwise
the command runs directly in the current shell.
"""
[docs]
def run(
self,
cmd: str | list[str],
env: dict[str, str],
cwd: Path,
conda_prefix: Path | None = None,
clean_env: bool = False,
) -> int:
"""Execute *cmd* and return the process exit code.
When *conda_prefix* is given the command runs inside an activated
conda environment. Otherwise it runs directly in the current shell.
"""
run_env = self._build_env(env, clean_env)
if isinstance(cmd, list):
cmd = " ".join(cmd)
if conda_prefix is not None:
return self._run_in_env(cmd, run_env, cwd, conda_prefix)
return self._run_direct(cmd, run_env, cwd)
def _build_env(self, extra: dict[str, str], clean: bool) -> dict[str, str]:
"""Build the environment variable mapping for a subprocess.
When *clean* is True only a minimal set of system variables is
kept (``PATH``, ``HOME``, etc.). *extra* variables are always merged in.
"""
if clean:
base: dict[str, str] = {}
for key in (
"PATH",
"HOME",
"USER",
"LOGNAME",
"SHELL",
"TERM",
"LANG",
"SYSTEMROOT",
"COMSPEC",
"TEMP",
"TMP",
):
val = os.environ.get(key)
if val is not None:
base[key] = val
else:
base = dict(os.environ)
base.update(extra)
return base
def _run_direct(self, cmd: str, env: dict[str, str], cwd: Path) -> int:
"""Run *cmd* in the native shell without conda activation."""
shell_cmd = self._shell_command(cmd)
result = subprocess.run(shell_cmd, env=env, cwd=str(cwd))
return result.returncode
def _run_in_env(
self,
cmd: str,
env: dict[str, str],
cwd: Path,
conda_prefix: Path,
) -> int:
"""Run *cmd* inside an activated conda environment at *conda_prefix*.
Uses ``conda.utils.wrap_subprocess_call`` to generate an
activation wrapper script, which is cleaned up after execution.
"""
from conda.base.context import context
from conda.utils import wrap_subprocess_call
root_prefix = context.root_prefix
dev_mode = context.dev
debug_wrapper_scripts: bool = getattr(context, "debug_wrapper_scripts", False)
script, command = wrap_subprocess_call(
root_prefix,
str(conda_prefix),
dev_mode,
debug_wrapper_scripts,
self._shell_command(cmd),
)
try:
result = subprocess.run(command, env=env, cwd=str(cwd))
return result.returncode
finally:
if script and Path(script).exists():
try:
Path(script).unlink()
except OSError:
pass
@staticmethod
def _shell_command(cmd: str) -> list[str]:
"""Wrap *cmd* in the platform-appropriate shell invocation."""
if on_win:
return ["cmd", "/d", "/c", cmd]
return [os.environ.get("SHELL", "/bin/sh"), "-c", cmd]