Files
lief-project-LIEF/api/python/backend/config.py
T
2026-03-20 15:40:36 +01:00

303 lines
10 KiB
Python

from __future__ import annotations
from pydantic_core import ErrorDetails, core_schema, CoreSchema
from pydantic import BaseModel, ValidationError, Field, GetCoreSchemaHandler
from typing import Any, Optional, Union, List, Literal
from pathlib import Path
import tomli
import os
import platform
from typing_extensions import Annotated
from scikit_build_core.settings.skbuild_read_settings import rich_print
def cmake_serialize(field: Any):
if isinstance(field, bool):
return "ON" if field else "OFF"
return field
class EnvStringValidator:
def _get_env_string(self, string: str) -> str:
winpy_architecture = os.getenv("LIEF_TARGET_ARCHITECTURE", "")
if winpy_architecture == "x86_64":
winpy_architecture = "amd64"
formatted = string.format(
python_version=os.getenv("LIEF_TARGET_PYTHON_VERSION", ""),
python_version_alt=os.getenv("LIEF_TARGET_PYTHON_VERSION", "").replace('.', ''),
architecture=os.getenv("LIEF_TARGET_ARCHITECTURE", ""),
winpy_architecture=winpy_architecture,
ci_project_dir=os.getenv("CI_PROJECT_DIR", ""),
stable_abi=os.getenv("LIEF_STABLE_ABI", ""),
free_threaded=os.getenv("LIEF_FREE_THREADED", ""),
runtime=os.getenv("LIEF_RUNTIME", "false"),
)
return formatted
def __get_pydantic_core_schema__(
self, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.no_info_after_validator_function(
self._get_env_string, handler(source_type)
)
EnvString = Annotated[str, EnvStringValidator()]
class BuildConfig(BaseModel):
build_type: EnvString = Field("Release", alias="type")
cache: bool = True
ninja: bool = False
stable_abi: Union[bool, EnvString] = Field(False, alias="stable-abi")
free_threaded: Union[bool, EnvString] = Field(False, alias="free-threaded")
default_target: str = Field("pyLIEF", alias="default-target")
parallel_jobs: int = Field(0, alias="parallel-jobs")
compilation_flags: List[str] = Field([], alias="compilation-flags")
build_dir: Optional[EnvString] = Field(None, alias="build-dir")
extra_targets: Union[List[EnvString], EnvString] = Field(None, alias="extra-targets")
extra_cmake: Union[List[EnvString], EnvString] = Field(None, alias="extra-cmake-opt")
lief_install_dir: Optional[EnvString] = Field(None, alias="lief-install-dir")
py_api: EnvString = Field("", alias="py-api")
c_compiler: Optional[EnvString] = Field(None, alias="c-compiler")
cxx_compiler: Optional[EnvString] = Field(None, alias="cxx-compiler")
@property
def targets(self) -> List[str]:
default_targets: List[str] = [self.default_target]
if self.extra_targets is not None:
if isinstance(self.extra_targets, str):
default_targets.append(self.extra_targets)
else:
default_targets.extend(self.extra_targets)
return default_targets
def cmake_dump(self) -> List[str]:
out: List[str] = [
f"-DLIEF_USE_CCACHE={cmake_serialize(self.cache)}"
]
if self.extra_cmake is not None:
if isinstance(self.extra_cmake, str):
out.append(self.extra_cmake)
elif isinstance(self.extra_cmake, list):
out.extend(self.extra_cmake)
if self.lief_install_dir is not None:
lief_cmake_dir = Path(self.lief_install_dir) / "lib" / "cmake" / "LIEF"
lief_dir = lief_cmake_dir.expanduser().resolve().absolute()
out.extend((
"-DLIEF_PY_LIEF_EXT=on",
"-DLIEF_INSTALL=off",
f"-DLIEF_DIR={lief_dir.as_posix()}"
))
if self.c_compiler is not None:
out.append(
f"-DCMAKE_C_COMPILER={self.c_compiler}"
)
if self.cxx_compiler is not None:
out.append(
f"-DCMAKE_CXX_COMPILER={self.cxx_compiler}"
)
if len(self.compilation_flags) > 0:
flags = " ".join(self.compilation_flags)
out.extend((
f'-DCMAKE_CXX_FLAGS={flags}',
f'-DCMAKE_C_FLAGS={flags}',
))
out.extend([
f"-DLIEF_PYTHON_STABLE_ABI={cmake_serialize(self.stable_abi)}",
f"-DLIEF_PYTHON_FREE_THREADED={cmake_serialize(self.free_threaded)}"
])
return out
class ThirdParty(BaseModel):
spdlog: Optional[EnvString] = None
nanobind: Optional[EnvString] = None
def cmake_dump(self) -> List[str]:
out: List[str] = []
if self.spdlog is not None:
out.extend((
"-DLIEF_EXTERNAL_SPDLOG=ON",
f"-Dspdlog_DIR={self.spdlog}"
))
if self.nanobind is not None:
out.extend((
"-DLIEF_OPT_NANOBIND_EXTERNAL=ON",
f"-Dnanobind_DIR={self.nanobind}"
))
return out
class CrossCompilation(BaseModel):
osx_arch: Optional[EnvString] = Field(None, alias="osx-arch")
platform: Optional[EnvString] = None
pyversion: Optional[EnvString] = None
abi: Optional[EnvString] = None
def cmake_dump(self) -> List[str]:
out: List[str] = []
if self.osx_arch is not None:
out.extend((
f'-DCMAKE_OSX_ARCHITECTURES={self.osx_arch}',
))
return out
class Formats(BaseModel):
elf: bool = True
pe: bool = True
macho: bool = True
coff: bool = True
dex: bool = True
art: bool = True
oat: bool = True
vdex: bool = True
def cmake_dump(self) -> List[str]:
return [
f"-DLIEF_ELF={cmake_serialize(self.elf)}",
f"-DLIEF_PE={cmake_serialize(self.pe)}",
f"-DLIEF_MACHO={cmake_serialize(self.macho)}",
f"-DLIEF_COFF={cmake_serialize(self.coff)}",
f"-DLIEF_DEX={cmake_serialize(self.dex)}",
f"-DLIEF_OAT={cmake_serialize(self.oat)}",
f"-DLIEF_ART={cmake_serialize(self.art)}",
f"-DLIEF_VDEX={cmake_serialize(self.vdex)}",
]
class Logging(BaseModel):
enabled: bool = True
debug: bool = False
def cmake_dump(self) -> List[str]:
return [
f"-DLIEF_LOGGING={cmake_serialize(self.enabled)}",
f"-DLIEF_LOGGING_DEBUG={cmake_serialize(self.debug)}",
]
class Features(BaseModel):
# json is an attribute already defined in BaseModel
json_support: bool = Field(True, alias="json")
frozen: bool = True
runtime: Union[bool, EnvString] = Field(False)
def cmake_dump(self) -> List[str]:
return [
f"-DLIEF_RUNTIME={cmake_serialize(self.runtime)}",
f"-DLIEF_ENABLE_JSON={cmake_serialize(self.json_support)}",
f"-DLIEF_DISABLE_FROZEN={cmake_serialize(not self.frozen)}",
]
class Runtime(BaseModel):
platform: Optional[Literal['linux', 'windows', 'android', 'osx', 'ios']] = None
architecture: Optional[Union[EnvString, Literal['arm64', 'x86_64']]] = None
def cmake_dump(self) -> List[str]:
out: List[str] = []
if self.platform is not None:
out.append(f"-DLIEF_RUNTIME_PLATFORM={self.platform}")
if self.architecture is not None:
out.append(f"-DLIEF_RUNTIME_ARCH={self.architecture}")
return out
class ConfigT(BaseModel):
build: BuildConfig = BuildConfig()
formats: Formats = Formats()
third_party: ThirdParty = Field(ThirdParty(), alias="third-party")
features: Features = Features()
logging: Logging = Logging()
runtime: Runtime = Runtime()
cross_compilation: CrossCompilation = Field(CrossCompilation(),
alias="cross-compilation")
def _cmake_base_args(self) -> List[str]:
return [
"-DLIEF_PYTHON_API=on",
"-DLIEF_INSTALL=off",
"-DLIEF_INSTALL_COMPILED_EXAMPLES=off",
]
def cmake_dump(self) -> List[str]:
return [
*self._cmake_base_args(),
*self.build.cmake_dump(),
*self.formats.cmake_dump(),
*self.third_party.cmake_dump(),
*self.features.cmake_dump(),
*self.logging.cmake_dump(),
*self.runtime.cmake_dump(),
*self.cross_compilation.cmake_dump(),
]
def pretty_error(err: ErrorDetails, file: Path):
loc = '.'.join(err['loc'])
if err['type'] == 'value_error.missing':
rich_print(f"[red]'{loc}' is missing in {file}")
else:
rich_print(f"[red]Error with '{loc}' in {file} ({err['msg']})")
class Config:
DEFAULT_EXCLUDE: List[str] = [
"*.so", "*.pyd"
]
def __init__(self, config: ConfigT):
self._config: ConfigT = config
@classmethod
def from_file(cls, file: Path):
with open(file, "rb") as f:
toml_config = tomli.load(f)
try:
return cls(ConfigT.model_validate(toml_config["lief"]))
except ValidationError as e:
for err in e.errors():
pretty_error(err, file)
return None
@property
def strip(self) -> str:
return str(self._config.build.build_type.lower() == "release")
@property
def build_dir(self) -> str:
if bdir := os.getenv("LIEF_BUILD_DIR", None):
return bdir
if bdir := self._config.build.build_dir:
return bdir
return ""
def get_cmake_args(self, editable: bool = False) -> List[str]:
cmake_args = self._config.cmake_dump()
if editable:
cmake_args.append(
"-DLIEF_PYTHON_EDITABLE=ON",
)
return cmake_args
@property
def cmake_generator(self) -> List[str]:
if self._config.build.ninja:
return ["-GNinja",]
return []
@property
def osx_arch(self) -> str:
return self.cross_compilation.osx_arch or platform.machine()
def __getattr__(self, name):
if value := getattr(self._config, name, None):
return value
return self.__getattribute__(name)