Files
lief-project-LIEF/api/python/setup.py
T
2023-07-09 21:33:43 +02:00

454 lines
15 KiB
Python

import os
import platform
import setuptools
import subprocess
import sys
import sysconfig
import tomli
from typing import List, Optional
from pathlib import Path
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
from shutil import copy2, which
CURRENT_DIR = Path(__file__).parent
LIEF_DIR = (CURRENT_DIR / ".." / "..").absolute().resolve()
PACKAGE_NAME = "lief"
PYLIEF_CONF_ENV = "PYLIEF_CONF"
DEFAULT_TARGET = "pyLIEF"
ENV = os.environ
cmake_conf = None
def report(*args):
print(*args)
class Config:
def __init__(self, config: dict):
self._config = config
if "cross-compilation" in self._config["lief"]:
cross_conf = self._config["lief"]["cross-compilation"]
if "platform" in cross_conf:
platform_build = cross_conf["platform"]
if os.name == "nt" and platform_build == "win32":
ENV["VSCMD_ARG_TGT_ARCH"] = "x86"
os.environ["VSCMD_ARG_TGT_ARCH"] = "x86"
@classmethod
def from_file(cls, file: Path):
with open(file, "rb") as f:
toml_config = tomli.load(f)
return cls(toml_config)
@staticmethod
def is_windows() -> bool:
return platform.system() == "Windows" and not sysconfig.get_platform().startswith("mingw")
def build_type(self) -> str:
return self._config['lief']['build']['type']
def get_lief_format_opt(self):
opt = self._config["lief"]["formats"]
return (
"-DLIEF_ELF={}".format("ON" if opt["elf"] else "OFF"),
"-DLIEF_PE={}".format("ON" if opt["pe"] else "OFF"),
"-DLIEF_MACHO={}".format("ON" if opt["macho"] else "OFF"),
"-DLIEF_DEX={}".format("ON" if opt["dex"] else "OFF"),
"-DLIEF_ART={}".format("ON" if opt["art"] else "OFF"),
"-DLIEF_OAT={}".format("ON" if opt["oat"] else "OFF"),
"-DLIEF_VDEX={}".format("ON" if opt["vdex"] else "OFF"),
)
def get_lief_logging_opt(self):
opt = self._config["lief"]["logging"]
return (
"-DLIEF_LOGGING={}".format("ON" if opt["enabled"] else "OFF"),
"-DLIEF_LOGGING_DEBUG={}".format("ON" if opt["debug"] else "OFF"),
)
def get_python_opt(self):
config = ["-DLIEF_PYTHON_API=on"]
interpreter = Path(sys.executable)
base = sysconfig.get_config_var("base")
if base is not None:
config += [f"-DPython_ROOT_DIR={base}"]
config += [
f"-DPython_EXECUTABLE={interpreter.as_posix()}",
]
return config
def get_generic_opt(self, output: str):
return (
f"-DCMAKE_BUILD_TYPE={self.build_type()}",
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={output}',
"-DLIEF_USE_CCACHE={}".format("ON" if self._config["lief"]["build"]["cache"] else "OFF"),
)
def get_feat_opt(self):
opt = self._config["lief"]["features"]
return (
"-DLIEF_ENABLE_JSON={}".format("ON" if opt["json"] else "OFF"),
"-DLIEF_DISABLE_FROZEN={}".format("OFF" if opt["frozen"] else "ON"),
)
def get_iwyu_opt(self):
if "include-what-you-use" in self._config["lief"]["build"]:
value = self._config["lief"]["build"]["include-what-you-use"]
return (f"-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE={value}", )
else:
return ("-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=''", )
def get_flags_opt(self):
opt = []
if "cxx-flags" in self._config["lief"]["build"]:
value = self._config["lief"]["build"]["cxx-flags"]
opt.append(f"-DCMAKE_CXX_FLAGS={value}")
else:
opt.append("-DCMAKE_CXX_FLAGS=''")
if "c-flags" in self._config["lief"]["build"]:
value = self._config["lief"]["build"]["c-flags"]
opt.append(f"-DCMAKE_C_FLAGS={value}")
else:
opt.append("-DCMAKE_C_FLAGS=''")
cxx_flags = os.getenv("CXXFLAGS", None)
c_flags = os.getenv("CFLAGS", None)
if cxx_flags is not None:
opt.append(f'-DCMAKE_CXX_FLAGS={cxx_flags}')
if c_flags is not None:
opt.append(f'-DCMAKE_C_FLAGS={c_flags}')
return opt
def get_cmake_extra_opt(self):
opt = []
if "extra-cmake-opt" in self._config["lief"]["build"]:
value = self._config["lief"]["build"]["extra-cmake-opt"]
if isinstance(value, str):
opt.append(value)
elif hasattr(value, "__iter__"):
opt.extend(value)
else:
print(f"Unsupported opt: {value} ({type(value)})")
sys.exit(1)
return opt
def get_third_party_opt(self) -> List[str]:
if "third-party" not in self._config["lief"]:
return []
cmake_opt = []
tp_conf = self._config["lief"]["third-party"]
if "spdlog" in tp_conf:
cmake_opt += [
"-DLIEF_EXTERNAL_SPDLOG=ON",
"-Dspdlog_DIR={}".format(tp_conf["spdlog"])
]
return cmake_opt
def get_cross_compile_opt(self) -> List[str]:
if "cross-compilation" not in self._config["lief"]:
return []
cmake_opt = []
cross_conf = self._config["lief"]["cross-compilation"]
if "osx-arch" in cross_conf:
cmake_opt += [
f'-DCMAKE_OSX_ARCHITECTURES={cross_conf["osx-arch"]}'
]
return cmake_opt
def get_pre_compile_opt(self):
install_path = self._config["lief"]["build"].get("lief-install-dir", None)
if install_path is None:
return []
install_path = Path(install_path).expanduser().resolve().absolute()
if not install_path.is_dir():
report(f"{install_path} is not a valid directory")
return []
cmake_dir = install_path / "share" / "LIEF" / "cmake"
if not (cmake_dir / "LIEFConfig.cmake").is_file():
report(f"Missing LIEFConfig.cmake in {cmake_dir}")
return []
return (
"-DLIEF_PY_LIEF_EXT=on",
"-DLIEF_INSTALL=off",
f"-DLIEF_DIR={cmake_dir}",
)
def gen_cmake_option(self, output: str) -> List[str]:
cfg = self.build_type()
opt = [
*self.get_generic_opt(output),
*self.get_python_opt(),
*self.get_lief_format_opt(),
*self.get_feat_opt(),
*self.get_iwyu_opt(),
*self.get_flags_opt(),
*self.get_third_party_opt(),
*self.get_pre_compile_opt(),
*self.get_cross_compile_opt(),
*self.get_cmake_extra_opt()
]
if self._use_ninja():
opt = ["-G", "Ninja"] + opt
if Config.is_windows():
is64 = sys.maxsize > 2**32
opt += [
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={output}',
f'-DLIEF_USE_CRT_{cfg.upper()}=MT',
]
if self._use_ninja():
from setuptools import msvc
arch = 'x64' if is64 else 'x86'
report(f"Arch: {arch}")
ninja_env = msvc.msvc14_get_vc_env(arch)
ENV.update(ninja_env)
else:
opt += ['-A', 'x64'] if is64 else ['-A', 'win32']
return opt
def _get_jobs(self) -> List[str]:
if "parallel-jobs" in self._config["lief"]["build"]:
jobs = self._config["lief"]["build"]["parallel-jobs"]
if jobs > 0:
return ["-j", str(jobs)]
return []
return []
def _get_targets(self) -> List[str]:
targets = [DEFAULT_TARGET]
if "extra-targets" in self._config["lief"]["build"]:
extra = self._config["lief"]["build"]["extra-targets"]
if isinstance(extra, str):
targets.append(extra)
elif hasattr(extra, "__iter__"):
targets.extend(extra)
else:
print(f"Unsupported opt: {targets} ({type(targets)})")
sys.exit(1)
return targets
def get_target_osx_arch(self) -> str:
if "cross-compilation" in self._config["lief"]:
cross_conf = self._config["lief"]["cross-compilation"]
if "osx-arch" in cross_conf:
arch = cross_conf["osx-arch"]
return arch
return platform.machine()
def _use_ninja(self) -> bool:
return self._config['lief']['build']['ninja']
def get_compile_cmd(self):
config = self._config['lief']['build']['type']
return [
'cmake',
'--build', '.',
'--target'] + self._get_targets() + [
'--config', config,
] + self._get_jobs()
class Versioning:
COMMAND = '{git} describe --tags --long --dirty'
GIT_BRANCH = '{git} rev-parse --abbrev-ref HEAD'
IS_TAGGED_CMD = '{git} tag --list --points-at=HEAD'
FMT_DEV = '{tag}.dev0'
FMT_TAGGED = '{tag}'
DEFAULT_VERSION = "0.14.0"
def __init__(self):
self._git = which("git")
def _exec_git_cmd(self, cmd: str, **kwargs) -> str:
args = dict(kwargs)
args["git"] = self._git
cmd_list = cmd.format(**args).split()
return subprocess.check_output(cmd_list).decode('utf-8').strip()
def has_git(self) -> bool:
return self._git is not None and (LIEF_DIR / ".git").is_dir()
def get_branch(self) -> Optional[str]:
if not self.has_git():
return None
try:
return self._exec_git_cmd(Versioning.GIT_BRANCH)
except subprocess.SubprocessError as e:
report(f"Error: {e}")
return None
def format_version(self, version: str, fmt: str, is_dev: bool = False):
branch = self.get_branch()
if branch is not None and branch.startswith("release-"):
_, version = branch.split("release-")
return version
parts = version.split('-')
assert len(parts) in (3, 4)
dirty = len(parts) == 4
tag, count, sha = parts[:3]
MA, MI, PA = map(int, tag.split(".")) # 0.9.0 -> (0, 9, 0)
if is_dev:
tag = f"{MA}.{MI + 1}.{0}"
if count == '0' and not dirty:
return tag
return fmt.format(tag=tag, gitsha=sha.lstrip('g'))
def version_from_git(self) -> str:
try:
is_tagged = self._exec_git_cmd(Versioning.IS_TAGGED_CMD) != ""
git_version = self._exec_git_cmd(Versioning.COMMAND)
if is_tagged:
return self.format_version(version=git_version, fmt=Versioning.FMT_TAGGED)
return self.format_version(version=git_version, fmt=Versioning.FMT_DEV, is_dev=True)
except Exception as e:
report(f"Error: {e}")
return Versioning.DEFAULT_VERSION
def get_version(self) -> str:
if self.has_git():
return self.version_from_git()
return Versioning.DEFAULT_VERSION
class LiefDistribution(setuptools.Distribution):
def __init__(self, attrs=None):
super().__init__(attrs)
class Module(Extension):
def __init__(self, name, sourcedir='', *args, **kwargs):
Extension.__init__(self, name, sources=[])
self.sourcedir = CURRENT_DIR.resolve().absolute().as_posix()
class BuildLibrary(build_ext):
def __init__(self, *args, **kwargs):
self._fix_platform()
super().__init__(*args, **kwargs)
def run(self):
for ext in self.extensions:
self.build_extension(ext)
self.copy_extensions_to_source()
def _fix_platform(self):
if sys.platform == "darwin":
deployment_target = os.getenv("MACOSX_DEPLOYMENT_TARGET", None)
machine = cmake_conf.get_target_osx_arch()
if deployment_target is None:
deployment_target = sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET")
os.environ["_PYTHON_HOST_PLATFORM"] = f"macosx-{deployment_target}-{machine}"
report("Using platform: ", os.environ["_PYTHON_HOST_PLATFORM"])
else:
if "cross-compilation" in cmake_conf._config["lief"]:
cross_conf = cmake_conf._config["lief"]["cross-compilation"]
if "platform" in cross_conf:
platform = cross_conf["platform"]
os.environ["_PYTHON_HOST_PLATFORM"] = platform
ENV["_PYTHON_HOST_PLATFORM"] = platform
report("Using platform: ", platform)
if os.name == "nt" and platform == "win32":
self.plat_name = "win32"
version_fixed = sys.version.lower().replace("amd64", "")
sys.version = version_fixed
sys.platform = "win32"
def build_extension(self, ext):
build_temp = Path(self.build_temp)
build_lib = Path(self.build_lib)
cmake_output = build_temp.parent.absolute()
cmake_bin = which("cmake")
if cmake_bin is None:
raise RuntimeError("Can't find cmake")
build_temp.mkdir(exist_ok=True, parents=True)
build_lib.mkdir(exist_ok=True, parents=True)
report(f"build-temp: {build_temp}")
report(f"build-lib: {build_lib}")
report(f"Platform : {platform.system()}")
report(f"Wheel library: {self.get_ext_fullname(ext.name)}")
cmake_subprocess_args = {
'cwd': build_temp.as_posix(),
'env': ENV,
}
# 1. Configure
configure_cmd = [cmake_bin, "-S", LIEF_DIR.as_posix()] + cmake_conf.gen_cmake_option(cmake_output)
report("CMake Config:", " ".join(configure_cmd))
subprocess.check_call(configure_cmd, **cmake_subprocess_args)
compile_cmd = cmake_conf.get_compile_cmd()
report("Compile with:"," ".join(compile_cmd))
subprocess.check_call(compile_cmd, **cmake_subprocess_args)
pylief_dst = build_lib / self.get_ext_filename(self.get_ext_fullname(ext.name))
libsuffix = pylief_dst.suffix
pylief_path = cmake_output / f"{PACKAGE_NAME}{libsuffix}"
if Config.is_windows():
pylief_base = cmake_output / "Release" / "api" / "python"
pylief_path = pylief_base / cmake_conf.build_type() / f"{PACKAGE_NAME}{libsuffix}"
if not pylief_path.is_file():
pylief_path = pylief_base / f"{PACKAGE_NAME}{libsuffix}"
pylief_path = pylief_path.as_posix()
dst = Path(pylief_dst)
dst.parent.mkdir(exist_ok=True)
report(f"Copying {pylief_path} into {pylief_dst}")
if not self.dry_run:
copy2(pylief_path, pylief_dst)
versioning = Versioning()
version = versioning.get_version()
report(f"Version is: {version}")
conf_env = os.getenv(PYLIEF_CONF_ENV, None)
conf_file = CURRENT_DIR / "config-default.toml" if conf_env is None else Path(conf_env).absolute().resolve()
cmake_conf = Config.from_file(conf_file)
cmdclass = {
'build_ext': BuildLibrary,
}
long_description = LIEF_DIR / "package" / "README.rst"
setup(
long_description=long_description.read_text(),
long_description_content_type="text/x-rst; charset=UTF-8",
distclass=LiefDistribution,
scripts=['examples/elf_reader.py', 'examples/pe_reader.py', 'examples/macho_reader.py'],
packages=["lief"],
package_data={"lief": ["py.typed", "*.pyi"]},
ext_modules=[Module("lief._lief")],
cmdclass=cmdclass,
version=version
)