Files
revng-revng/python/revng/cli/support.py
Giacomo Vercesi e5c9141ccc Refactor revng.cli.support
* Removed shadowed `relative` function
* Clean up imports
* Add type hints to functions
* Use Path where possible
2022-04-26 15:05:30 +02:00

224 lines
6.6 KiB
Python

#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
import os
import shlex
import signal
import subprocess
import sys
from dataclasses import dataclass
from itertools import chain
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from elftools.elf.dynamic import DynamicSegment
from elftools.elf.elffile import ELFFile
try:
from shutil import which
except ImportError:
from backports.shutil_which import which # type: ignore
@dataclass
class Options:
parsed_args: Any
remaining_args: List[str]
search_prefixes: List[str]
command_prefix: List[str]
verbose: bool
dry_run: bool
keep_temporaries: bool
def shlex_join(split_command: Iterable[str]) -> str:
return " ".join(shlex.quote(arg) for arg in split_command)
def log_error(msg: str):
sys.stderr.write(msg + "\n")
def wrap(args: List[str], command_prefix: List[str]):
return command_prefix + args
def relative(path: str) -> str:
return os.path.relpath(path, os.getcwd())
def run(command, options: Options, environment: Optional[Dict[str, str]] = None):
if is_executable(command[0]):
command = wrap(command, options.command_prefix)
if options.verbose:
program_path = relative(command[0])
sys.stderr.write("{}\n\n".format(" \\\n ".join([program_path] + command[1:])))
if options.dry_run:
return
signal.signal(signal.SIGINT, signal.SIG_IGN)
if environment is None:
environment = dict(os.environ)
p = subprocess.Popen(
command, preexec_fn=lambda: signal.signal(signal.SIGINT, signal.SIG_DFL), env=environment
)
if p.wait() != 0:
log_error(f"The following command exited with {p.returncode}:\n{shlex_join(command)}")
sys.exit(p.returncode)
def is_executable(path: str) -> bool:
with open(path, "rb") as program:
return program.read(4) == b"\x7fELF"
def is_dynamic(path: str) -> bool:
with open(path, "rb") as input_file:
return (
len(
[
segment
for segment in ELFFile(input_file).iter_segments()
if segment.header.p_type == "PT_DYNAMIC"
]
)
!= 0
)
def get_command(command: str, search_prefixes: Iterable[str]) -> str:
if command.startswith("revng-"):
for executable in collect_files(search_prefixes, ["libexec", "revng"], command):
return executable
log_error(f'Couldn\'t find "{command}"')
assert False
path = which(command)
if not path:
log_error('Couldn\'t find "{command}".')
assert False
return os.path.abspath(path)
def interleave(base: List[str], repeat: str):
return list(sum(zip([repeat] * len(base), base), ()))
# Use in case different version of pyelftools might give str or bytes
def to_string(obj: Union[str, bytes]) -> str:
if isinstance(obj, str):
return obj
return obj.decode("utf-8")
def get_elf_needed(path: str) -> List[str]:
with open(path, "rb") as elf_file:
segments = [
segment
for segment in ELFFile(elf_file).iter_segments()
if isinstance(segment, DynamicSegment)
]
if len(segments) != 1:
return []
needed = [
to_string(tag.needed)
for tag in segments[0].iter_tags()
if tag.entry.d_tag == "DT_NEEDED"
]
elf_runpath: List[str] = [
tag.runpath for tag in segments[0].iter_tags() if tag.entry.d_tag == "DT_RUNPATH"
]
assert len(elf_runpath) < 2
if not elf_runpath:
return needed
runpaths = [
runpath.replace("$ORIGIN", str(Path(path).parent))
for runpath in elf_runpath[0].split(":")
]
absolute_needed = []
for lib in needed:
found = False
for runpath in runpaths:
full_path = Path(runpath) / lib
if full_path.is_file():
absolute_needed.append(str(full_path.resolve()))
found = True
break
if not found:
absolute_needed.append(lib)
return absolute_needed
def collect_files(
search_prefixes: Iterable[str], path_components: Iterable[str], pattern: str
) -> List[str]:
to_load: Dict[str, Path] = {}
for prefix in search_prefixes:
analyses_path = Path(prefix).joinpath(*path_components)
if not analyses_path.is_dir():
continue
# Enumerate all the libraries containing analyses
for library in analyses_path.glob(pattern):
if library.name not in to_load:
to_load[library.name] = library.resolve()
return [str(v) for v in to_load.values()]
def collect_libraries(search_prefixes: Iterable[str]) -> Tuple[List[str], Set[str]]:
to_load = collect_files(search_prefixes, ["lib", "revng", "analyses"], "*.so")
# Identify all the libraries that are dependencies of other libraries, i.e.,
# non-roots in the dependencies tree. Note that circular dependencies are
# not allowed.
dependencies = set(chain.from_iterable([get_elf_needed(path) for path in to_load]))
return (to_load, dependencies)
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"
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'LD_PRELOAD={libasan_path} ASAN_OPTIONS={new_asan_options} exec "$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
)