Files
lief-project-LIEF/api/python/setup.py
T
Romain Thomas cfe89fdc3c Bump version
2023-04-15 13:16:25 +02:00

370 lines
13 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"
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 = cross_conf["platform"]
if os.name == "nt" and platform == "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 gen_cmake_option(self, output: str) -> List[str]:
cfg = self.build_type()
opt = [
f"-DCMAKE_BUILD_TYPE={cfg}",
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={output}',
"-DLIEF_USE_CCACHE={}".format("ON" if self._config["lief"]["build"]["cache"] else "OFF"),
] + [
"-DLIEF_FORCE_API_EXPORTS=ON",
"-DLIEF_PYTHON_API=on",
f"-DPYTHON_EXECUTABLE={sys.executable}",
] + [
"-DLIEF_ELF={}".format("ON" if self._config["lief"]["formats"]["elf"] else "OFF"),
"-DLIEF_PE={}".format("ON" if self._config["lief"]["formats"]["pe"] else "OFF"),
"-DLIEF_MACHO={}".format("ON" if self._config["lief"]["formats"]["macho"] else "OFF"),
"-DLIEF_DEX={}".format("ON" if self._config["lief"]["formats"]["dex"] else "OFF"),
"-DLIEF_ART={}".format("ON" if self._config["lief"]["formats"]["art"] else "OFF"),
"-DLIEF_OAT={}".format("ON" if self._config["lief"]["formats"]["oat"] else "OFF"),
"-DLIEF_VDEX={}".format("ON" if self._config["lief"]["formats"]["vdex"] else "OFF"),
] + [
"-DLIEF_LOGGING={}".format("ON" if self._config["lief"]["logging"]["enabled"] else "OFF"),
"-DLIEF_LOGGING_DEBUG={}".format("ON" if self._config["lief"]["logging"]["debug"] else "OFF"),
] + [
"-DLIEF_ENABLE_JSON={}".format("ON" if self._config["lief"]["features"]["json"] else "OFF"),
"-DLIEF_DISABLE_FROZEN={}".format("OFF" if self._config["lief"]["features"]["frozen"] else "ON"),
] + [
"-DLIEF_ENABLE_JSON={}".format("ON" if self._config["lief"]["features"]["json"] else "OFF"),
"-DLIEF_DISABLE_FROZEN={}".format("OFF" if self._config["lief"]["features"]["frozen"] else "ON"),
]
if "include-what-you-use" in self._config["lief"]["build"]:
value = self._config["lief"]["build"]["include-what-you-use"]
opt.append(f"-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE={value}")
else:
opt.append("-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=''")
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}')
if self._use_ninja():
opt = ["-G", "Ninja"] + opt
opt += self._get_third_party_opt()
opt += self._get_cross_compile_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_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_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_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 _win_compile_cmd(self):
return [
'cmake',
'--build', '.',
'--target', 'pyLIEF',
'--config', self._config['lief']['build']['type'],
]
def _unix_compile_cmd(self):
gen = which("ninja") if self._use_ninja() else which("make")
return [gen] + self._get_jobs() + ["pyLIEF"]
def get_compile_cmd(self):
if Config.is_windows():
return self._win_compile_cmd()
return self._unix_compile_cmd()
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(" ".join(configure_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
)