Files
Ole André Vadla Ravnås 07e5aa86ee env: Reject pkg-config ignoring PKG_CONFIG_LIBDIR
The pkgconf-pypi shim silently does nothing when it is set, which made
Meson fall back to building all dependencies from source.
2026-10-29 10:15:57 +01:00

483 lines
16 KiB
Python

from collections import OrderedDict
from configparser import ConfigParser
from dataclasses import dataclass
import os
from pathlib import Path
import platform
import pprint
import shlex
import shutil
import subprocess
import sys
from typing import Callable, Dict, List, Literal, Optional, Tuple
from . import env_android, env_apple, env_generic, machine_file
from .machine_file import bool_to_meson, str_to_meson, strv_to_meson
from .machine_spec import MachineSpec
@dataclass
class MachineConfig:
machine_file: Path
binpath: List[Path]
environ: Dict[str, str]
def make_merged_environment(self, source_environ: Dict[str, str]) -> Dict[str, str]:
menv = {**source_environ}
menv.update(self.environ)
if self.binpath:
old_path = menv.get("PATH", "")
old_dirs = old_path.split(os.pathsep) if old_path else []
menv["PATH"] = os.pathsep.join([str(p) for p in self.binpath] + old_dirs)
return menv
DefaultLibrary = Literal["shared", "static"]
def call_meson(argv, use_submodule, *args, **kwargs):
return subprocess.run(query_meson_entrypoint(use_submodule) + argv, *args, **kwargs)
def query_meson_entrypoint(use_submodule):
if use_submodule:
return [sys.executable, str(INTERNAL_MESON_ENTRYPOINT)]
return ["meson"]
def load_meson_config(machine: MachineSpec, flavor: str, build_dir: Path):
return machine_file.load(query_machine_file_path(machine, flavor, build_dir))
def query_machine_file_path(machine: MachineSpec, flavor: str, build_dir: Path) -> Path:
return build_dir / f"frida{flavor}-{machine.identifier}.txt"
def detect_default_prefix() -> Path:
if platform.system() == "Windows":
return Path(os.environ["ProgramFiles"]) / "Frida"
return Path("/usr/local")
def generate_machine_configs(build_machine: MachineSpec,
host_machine: MachineSpec,
environ: Dict[str, str],
toolchain_prefix: Optional[Path],
build_sdk_prefix: Optional[Path],
host_sdk_prefix: Optional[Path],
call_selected_meson: Callable,
default_library: DefaultLibrary,
outdir: Path,
apple_min_os: Optional[Dict[str, str]] = None) -> Tuple[MachineConfig, MachineConfig]:
is_cross_build = host_machine != build_machine
if is_cross_build:
build_environ = {build_envvar_to_host(k): v for k, v in environ.items() if k not in TOOLCHAIN_ENVVARS}
else:
build_environ = environ
build_config = \
generate_machine_config(build_machine,
build_machine,
is_cross_build,
build_environ,
toolchain_prefix,
build_sdk_prefix,
call_selected_meson,
default_library,
outdir,
apple_min_os)
if is_cross_build:
host_config = generate_machine_config(host_machine,
build_machine,
is_cross_build,
environ,
toolchain_prefix,
host_sdk_prefix,
call_selected_meson,
default_library,
outdir,
apple_min_os)
else:
host_config = build_config
return (build_config, host_config)
def generate_machine_config(machine: MachineSpec,
build_machine: MachineSpec,
is_cross_build: bool,
environ: Dict[str, str],
toolchain_prefix: Optional[Path],
sdk_prefix: Optional[Path],
call_selected_meson: Callable,
default_library: DefaultLibrary,
outdir: Path,
apple_min_os: Optional[Dict[str, str]] = None) -> MachineConfig:
config = ConfigParser(dict_type=OrderedDict, interpolation=None)
config["constants"] = OrderedDict()
config["binaries"] = OrderedDict()
config["built-in options"] = OrderedDict()
config["properties"] = OrderedDict()
config["host_machine"] = OrderedDict([
("system", str_to_meson(machine.system)),
("subsystem", str_to_meson(machine.subsystem)),
("kernel", str_to_meson(machine.kernel)),
("cpu_family", str_to_meson(machine.cpu_family)),
("cpu", str_to_meson(machine.cpu)),
("endian", str_to_meson(machine.endian)),
])
binaries = config["binaries"]
builtin_options = config["built-in options"]
properties = config["properties"]
outpath = []
outenv = OrderedDict()
outdir.mkdir(parents=True, exist_ok=True)
if machine.is_apple:
impl = env_apple
elif machine.os == "android":
impl = env_android
else:
impl = env_generic
impl.init_machine_config(machine,
build_machine,
is_cross_build,
environ,
toolchain_prefix,
sdk_prefix,
call_selected_meson,
config,
outpath,
outenv,
outdir,
apple_min_os=apple_min_os)
if machine.toolchain_is_msvc:
builtin_options["b_vscrt"] = str_to_meson(machine.config)
pkg_config = None
vala_compiler = None
if toolchain_prefix is not None:
toolchain_bindir = toolchain_prefix / "bin"
exe_suffix = build_machine.executable_suffix
ninja_binary = toolchain_bindir / f"ninja{exe_suffix}"
if ninja_binary.exists():
outenv["NINJA"] = str(ninja_binary)
for (tool_name, filename_suffix) in {("gdbus-codegen", ""),
("gio-querymodules", exe_suffix),
("glib-compile-resources", exe_suffix),
("glib-compile-schemas", exe_suffix),
("glib-genmarshal", ""),
("glib-mkenums", ""),
("flex", exe_suffix),
("bison", exe_suffix),
("nasm", exe_suffix)}:
tool_path = toolchain_bindir / (tool_name + filename_suffix)
if tool_path.exists():
if tool_name == "bison":
outenv["BISON_PKGDATADIR"] = str(toolchain_prefix / "share" / "bison")
outenv["M4"] = str(toolchain_bindir / f"m4{exe_suffix}")
else:
tool_path = shutil.which(tool_name)
if tool_path is not None:
binaries[tool_name] = strv_to_meson([str(tool_path)])
if machine == build_machine:
pkg_config_binary = find_usable_system_pkg_config()
else:
pkg_config_binary = None
if pkg_config_binary is None:
pkg_config_binary = toolchain_bindir / f"pkg-config{exe_suffix}"
if not pkg_config_binary.exists():
pkg_config_binary = None
if pkg_config_binary is not None:
pkg_config = [
str(pkg_config_binary),
]
if sdk_prefix is not None:
if default_library == "static":
pkg_config += ["--static"]
pkg_config += [f"--define-variable=frida_sdk_prefix={sdk_prefix}"]
vala_compiler = detect_toolchain_vala_compiler(toolchain_prefix, build_machine)
pkg_config_path = shlex.split(environ.get("PKG_CONFIG_PATH", "").replace("\\", "\\\\"))
pkg_config_libdir = None
if sdk_prefix is not None:
builtin_options["vala_args"] = strv_to_meson([
"--vapidir=" + str(sdk_prefix / "share" / "vala" / "vapi")
])
pkg_config_libdir = str(sdk_prefix / machine.libdatadir / "pkgconfig")
sdk_bindir = sdk_prefix / "bin" / build_machine.os_dash_arch
if sdk_bindir.exists():
for f in sdk_bindir.iterdir():
binaries[f.stem] = strv_to_meson([str(f)])
if pkg_config is not None:
wrapper = outdir / f"frida-pkg-config-{machine.identifier}.py"
wrapper.write_text(make_pkg_config_wrapper(pkg_config, pkg_config_path, pkg_config_libdir), encoding="utf-8")
pkg_config = [sys.executable, str(wrapper)]
binaries["pkg-config"] = strv_to_meson(pkg_config)
if vala_compiler is not None:
valac, vapidir = vala_compiler
vala = [
str(valac),
f"--vapidir={vapidir}",
]
if pkg_config is not None:
vala += [f"--pkg-config={quote(sys.executable)} {quote(str(wrapper))}"]
binaries["vala"] = strv_to_meson(vala)
qmake6 = shutil.which("qmake6")
if qmake6 is not None:
binaries["qmake6"] = strv_to_meson([qmake6])
builtin_options["pkg_config_path"] = strv_to_meson(pkg_config_path)
needs_wrapper = needs_exe_wrapper(build_machine, machine, environ)
properties["needs_exe_wrapper"] = bool_to_meson(needs_wrapper)
if needs_wrapper:
wrapper = find_exe_wrapper(machine, environ)
if wrapper is not None:
binaries["exe_wrapper"] = strv_to_meson(wrapper)
target_abi = env_generic.target_abi_of(machine)
if target_abi is not None:
properties["target_abi"] = str_to_meson(target_abi)
if machine.is_freestanding:
properties["have_c99_snprintf"] = bool_to_meson(True)
properties["have_c99_vsnprintf"] = bool_to_meson(True)
properties["have_unix98_printf"] = bool_to_meson(True)
machine_file = outdir / f"frida-{machine.identifier}.txt"
with machine_file.open("w", encoding="utf-8") as f:
config.write(f)
return MachineConfig(machine_file, outpath, outenv)
def needs_exe_wrapper(build_machine: MachineSpec,
host_machine: MachineSpec,
environ: Dict[str, str]) -> bool:
return not can_run_host_binaries(build_machine, host_machine, environ)
def can_run_host_binaries(build_machine: MachineSpec,
host_machine: MachineSpec,
environ: Dict[str, str]) -> bool:
if host_machine == build_machine:
return True
build_os = build_machine.os
build_arch = build_machine.arch
host_os = host_machine.os
host_arch = host_machine.arch
if host_os == build_os:
if build_os == "windows":
return build_arch == "arm64" or host_arch != "arm64"
if build_os == "macos":
if build_arch == "arm64" and host_arch == "x86_64":
return True
if build_os == "linux" and host_machine.config == build_machine.config:
if build_arch == "x86_64" and host_arch == "x86":
return True
return environ.get("FRIDA_CAN_RUN_HOST_BINARIES", "no") == "yes"
def find_exe_wrapper(machine: MachineSpec,
environ: Dict[str, str]) -> Optional[List[str]]:
if machine.arch == "arm64beilp32":
return None
qemu_sysroot = environ.get("FRIDA_QEMU_SYSROOT")
if qemu_sysroot is None:
return None
qemu_flavor = "qemu-" + QEMU_ARCHS.get(machine.arch, machine.arch)
qemu_binary = shutil.which(qemu_flavor)
if qemu_binary is None:
raise QEMUNotFoundError(f"unable to find {qemu_flavor}, needed due to FRIDA_QEMU_SYSROOT being set")
return [qemu_binary, "-L", qemu_sysroot]
def find_usable_system_pkg_config() -> Optional[str]:
binary = shutil.which("pkg-config")
if binary is None:
return None
try:
helptext = subprocess.run([binary, "--help"],
capture_output=True,
text=True,
timeout=5).stdout
except (OSError, subprocess.SubprocessError):
return None
is_strawberry_perl_stub = "Pure-Perl" in helptext
if is_strawberry_perl_stub:
return None
if not responds_when_libdir_is_set(binary):
return None
return binary
def responds_when_libdir_is_set(pkg_config: str) -> bool:
version = subprocess.run([pkg_config, "--version"],
capture_output=True,
text=True,
env={**os.environ, "PKG_CONFIG_LIBDIR": os.devnull}).stdout
return version.strip() != ""
def make_pkg_config_wrapper(pkg_config: List[str], pkg_config_path: List[str], pkg_config_libdir: Optional[str] = None) -> str:
lines = [
"import os",
"import subprocess",
"import sys",
"",
"args = [",
f" {pprint.pformat(pkg_config, indent=4)[1:-1]},",
" *sys.argv[1:],",
"]",
"env = {**os.environ}",
]
if pkg_config_path:
joined = os.pathsep.join(pkg_config_path)
sep = os.pathsep
lines.append(f"env['PKG_CONFIG_PATH'] = {joined!r} + {sep!r}"
f" + env.get('PKG_CONFIG_PATH', '')")
if pkg_config_libdir is not None:
lines.append(f"env['PKG_CONFIG_LIBDIR'] = {pkg_config_libdir!r}")
lines += [
"p = subprocess.run(args, env=env)",
"sys.exit(p.returncode)",
]
return "\n".join(lines)
def detect_toolchain_vala_compiler(toolchain_prefix: Path,
build_machine: MachineSpec) -> Optional[Tuple[Path, Path]]:
datadir = next((toolchain_prefix / "share").glob("vala-*"), None)
if datadir is None:
return None
api_version = datadir.name.split("-", maxsplit=1)[1]
valac = toolchain_prefix / "bin" / f"valac-{api_version}{build_machine.executable_suffix}"
vapidir = datadir / "vapi"
return (valac, vapidir)
def build_envvar_to_host(name: str) -> str:
if name.endswith("_FOR_BUILD"):
return name[:-10]
return name
def quote(s: str) -> str:
if " " not in s:
return s
return "\"" + s.replace("\"", "\\\"") + "\""
class QEMUNotFoundError(Exception):
pass
INTERNAL_MESON_ENTRYPOINT = Path(__file__).resolve().parent / "meson" / "meson.py"
# Based on mesonbuild/envconfig.py and mesonbuild/compilers/compilers.py
TOOLCHAIN_ENVVARS = {
# Compilers
"CC",
"CXX",
"CSC",
"CYTHON",
"DC",
"FC",
"OBJC",
"OBJCXX",
"RUSTC",
"VALAC",
"NASM",
# Linkers
"CC_LD",
"CXX_LD",
"DC_LD",
"FC_LD",
"OBJC_LD",
"OBJCXX_LD",
"RUSTC_LD",
# Binutils
"AR",
"AS",
"LD",
"NM",
"OBJCOPY",
"OBJDUMP",
"RANLIB",
"READELF",
"SIZE",
"STRINGS",
"STRIP",
"WINDRES",
# Other tools
"CMAKE",
"QMAKE",
"PKG_CONFIG",
"PKG_CONFIG_PATH",
"MAKE",
"VAPIGEN",
"LLVM_CONFIG",
# Deprecated
"D_LD",
"F_LD",
"RUST_LD",
"OBJCPP_LD",
# Flags
"CFLAGS",
"CXXFLAGS",
"CUFLAGS",
"OBJCFLAGS",
"OBJCXXFLAGS",
"FFLAGS",
"DFLAGS",
"VALAFLAGS",
"RUSTFLAGS",
"CYTHONFLAGS",
"CSFLAGS",
"LDFLAGS",
}
QEMU_ARCHS = {
"armeabi": "arm",
"armhf": "arm",
"armbe8": "armeb",
"arm64": "aarch64",
"arm64be": "aarch64_be",
}