mirror of
https://github.com/revng/revng
synced 2026-06-21 14:07:57 +00:00
5287e69be0
Expose redirecting std{in,out,err} to `run`, `try_run` and `popen`.
242 lines
7.0 KiB
Python
242 lines
7.0 KiB
Python
#
|
|
# This file is distributed under the MIT License. See LICENSE.md for details.
|
|
#
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shlex
|
|
import signal
|
|
import sys
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from subprocess import Popen
|
|
from tempfile import NamedTemporaryFile
|
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Literal, Mapping, NoReturn
|
|
from typing import Optional, Tuple, Union
|
|
|
|
import yaml
|
|
|
|
from revng.internal.support.collect import collect_libraries
|
|
from revng.internal.support.elf import is_executable
|
|
from revng.support import TarDictionary, get_command
|
|
|
|
if TYPE_CHECKING:
|
|
from _typeshed import FileDescriptorLike
|
|
|
|
OptionalEnv = Optional[Mapping[str, str]]
|
|
|
|
|
|
@dataclass
|
|
class Options:
|
|
parsed_args: Any
|
|
remaining_args: List[str]
|
|
command_prefix: List[str]
|
|
verbose: bool
|
|
dry_run: bool
|
|
keep_temporaries: bool
|
|
search_prefixes: List[str]
|
|
|
|
|
|
def shlex_join(split_command: Iterable[str]) -> str:
|
|
return " ".join(shlex.quote(arg) for arg in split_command)
|
|
|
|
|
|
def wrap(args: List[str], command_prefix: List[str]):
|
|
return command_prefix + args
|
|
|
|
|
|
def relative(path: str) -> str:
|
|
relative_path = os.path.relpath(path, os.getcwd())
|
|
if len(relative_path) < len(path):
|
|
return relative_path
|
|
else:
|
|
return path
|
|
|
|
|
|
def _run_common(
|
|
command, options: Options, environment: OptionalEnv = None
|
|
) -> Tuple[List[str], Dict[str, str]]:
|
|
if not os.path.isfile(command[0]):
|
|
command = [get_command(command[0], options.search_prefixes), *command[1:]]
|
|
|
|
if len(options.command_prefix) > 0:
|
|
if is_executable(command[0]):
|
|
command = wrap(command, options.command_prefix)
|
|
else:
|
|
sh = get_command("sh", options.search_prefixes)
|
|
command = wrap([sh, "-c", 'exec "$0" "$@"', *command], options.command_prefix)
|
|
|
|
if options.verbose:
|
|
program_path = relative(command[0])
|
|
sys.stderr.write("{}\n\n".format(" \\\n ".join([program_path] + command[1:])))
|
|
|
|
environment = dict(os.environ if environment is None else environment)
|
|
|
|
if "valgrind" in command:
|
|
environment["PYTHONMALLOC"] = "malloc"
|
|
|
|
return command, environment
|
|
|
|
|
|
def _popen(command, options: Options, environment: OptionalEnv = None, **kwargs) -> int | Popen:
|
|
command, environment = _run_common(command, options, environment)
|
|
|
|
if options.dry_run:
|
|
return 0
|
|
|
|
return Popen(command, env=environment, **kwargs)
|
|
|
|
|
|
def popen(
|
|
command,
|
|
options: Options,
|
|
environment: OptionalEnv = None,
|
|
stdin: FileDescriptorLike | None = None,
|
|
stdout: FileDescriptorLike | None = None,
|
|
stderr: FileDescriptorLike | None = None,
|
|
) -> int | Popen:
|
|
return _popen(command, options, environment, stdin=stdin, stdout=stdout, stderr=stderr)
|
|
|
|
|
|
def try_run(
|
|
command,
|
|
options: Options,
|
|
environment: OptionalEnv = None,
|
|
stdin: FileDescriptorLike | None = None,
|
|
stdout: FileDescriptorLike | None = None,
|
|
stderr: FileDescriptorLike | None = None,
|
|
) -> int:
|
|
try:
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
process = _popen(
|
|
command,
|
|
options,
|
|
environment,
|
|
preexec_fn=lambda: signal.signal(signal.SIGINT, signal.SIG_DFL),
|
|
close_fds=False,
|
|
stdin=stdin,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
)
|
|
if isinstance(process, int):
|
|
return process
|
|
return process.wait()
|
|
finally:
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
|
|
|
|
def run(
|
|
command,
|
|
options: Options,
|
|
environment: OptionalEnv = None,
|
|
stdin: FileDescriptorLike | None = None,
|
|
stdout: FileDescriptorLike | None = None,
|
|
stderr: FileDescriptorLike | None = None,
|
|
):
|
|
result = try_run(command, options, environment, stdin, stdout, stderr)
|
|
if result != 0:
|
|
sys.exit(result)
|
|
|
|
return result
|
|
|
|
|
|
def exec_run(command, options: Options, environment: OptionalEnv) -> Union[int, NoReturn]:
|
|
command, environment = _run_common(command, options, environment)
|
|
|
|
if options.dry_run:
|
|
return 0
|
|
|
|
return os.execvpe(command[0], command, environment)
|
|
|
|
|
|
def interleave(base: List[str], repeat: str):
|
|
return list(sum(zip([repeat] * len(base), base), ()))
|
|
|
|
|
|
def handle_asan(dependencies: Iterable[str], search_prefixes: Iterable[str]) -> List[str]:
|
|
libasan = [name for name in dependencies if ("libasan." in name or "libclang_rt.asan" in name)]
|
|
|
|
if len(libasan) != 1:
|
|
return []
|
|
|
|
libasan_path = relative(libasan[0])
|
|
original_asan_options = os.environ.get("ASAN_OPTIONS", "")
|
|
if original_asan_options:
|
|
asan_options = dict([option.split("=") for option in original_asan_options.split(":")])
|
|
else:
|
|
asan_options = {}
|
|
asan_options["abort_on_error"] = "1"
|
|
asan_options["detect_leaks"] = "0"
|
|
new_asan_options = ":".join(["=".join(option) for option in asan_options.items()])
|
|
|
|
# Use `sh` instead of `env` since `env` sometimes is not a real executable
|
|
# but a shebang script spawning /usr/bin/coreutils, which makes gdb unhappy
|
|
return [
|
|
get_command("sh", search_prefixes),
|
|
"-c",
|
|
f'ASAN_OPTIONS={new_asan_options} ld.so --preload {libasan_path} "$0" "$@"',
|
|
]
|
|
|
|
|
|
def build_command_with_loads(command: str, args: Iterable[str], options: Options) -> List[str]:
|
|
(to_load, dependencies) = collect_libraries(options.search_prefixes)
|
|
prefix = handle_asan(dependencies, options.search_prefixes)
|
|
|
|
return (
|
|
prefix
|
|
+ [relative(get_command(command, options.search_prefixes))]
|
|
+ interleave(to_load, "-load")
|
|
+ args
|
|
)
|
|
|
|
|
|
def executable_name() -> str:
|
|
return os.path.basename(sys.argv[0])
|
|
|
|
|
|
def to_string(filename: str, raw: bytes) -> str:
|
|
return raw.decode("utf8")
|
|
|
|
|
|
def extract_tar[T](raw: bytes, process: Callable[[str, bytes], Any] = to_string) -> Dict[str, Any]:
|
|
return {key: process(key, value) for key, value in TarDictionary(raw).items()}
|
|
|
|
|
|
def to_yaml(filename: str, raw: bytes) -> str:
|
|
return yaml.safe_load(raw)
|
|
|
|
|
|
def is_file_executable(filename: str) -> bool:
|
|
stat = os.stat(filename)
|
|
return stat.st_mode & 0o111 == 0o111
|
|
|
|
|
|
def temporary_file_gen(prefix: str, options: Options):
|
|
def temporary_file(suffix="", mode="w+"):
|
|
return NamedTemporaryFile(
|
|
prefix=prefix,
|
|
suffix=suffix,
|
|
mode=mode,
|
|
delete=not options.keep_temporaries,
|
|
)
|
|
|
|
return temporary_file
|
|
|
|
|
|
@contextmanager
|
|
def file_wrapper(path: str | None, mode: Literal["r", "rb", "w", "wb"]):
|
|
"""Automatic wrapper for `open` for command-line arguments, to be used in
|
|
a `with` statement. Will automatically return stdin/stdout based on `mode`
|
|
in case the provided path is None or "-".
|
|
"""
|
|
assert mode in ("r", "rb", "w", "wb")
|
|
if path is None or path == "-":
|
|
if "r" in mode:
|
|
yield sys.stdin.buffer if "b" in mode else sys.stdin
|
|
else:
|
|
yield sys.stdout.buffer if "b" in mode else sys.stdout
|
|
else:
|
|
with open(path, mode) as f:
|
|
yield f
|