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 )