fester/backend/toolchain/chroot.py

822 lines
28 KiB
Python
Executable File

"""Chroot-based toolchain provider for Fester.
Fester can use any distro chroot as a build environment, not just BTC.sh.
The chroot provider supports:
- **native** — use the host system's toolchain (ccache gcc/g++)
- **buildroot** — use a Buildroot output tree as a cross-compiler sysroot
- **sourcemage** — use a SourceMage GL test chroot as the build environment
- **gentoo** — use a Gentoo stage3 chroot as the build environment
- **lede** — use a LEDE/OpenWrt SDK as a cross-compiler (mipsel/arm)
- **lunar** — use a Lunar Linux chroot as the build environment
- **chroot** — generic distro chroot (auto-detect toolchain inside)
Each provider probes for the chroot/sysroot on disk and, if available,
returns a compiler environment dict (CC, CXX, CFLAGS, CXXFLAGS, LDFLAGS,
PATH) that is merged into the executor environment.
The provider is selected via the ``toolchain.provider`` config key or
per-node via ``toolchain.<provider_name>`` in node config.
Design follows CLFS / Buildroot patterns:
- Cross-compilers are detected by their triple prefix in the chroot's
/usr/bin/ directory (e.g., arm-linux-gnueabihf-gcc).
- The chroot's /lib, /usr/lib, and /usr/include are used as implicit
sysroots (via --sysroot or -I/-L flags).
- For buildroot, the output/host/ and output/staging/ trees are used
directly (Buildroot's standard layout).
"""
import logging
import os
import re
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Provider registry — maps provider name → probe function
# ---------------------------------------------------------------------------
# Table-driven provider lookup (PEP 868 / SEI CERT CTR50-JP).
_PROVIDERS: Dict[str, "ProviderSpec"] = {}
class ProviderSpec:
"""Metadata and probe function for a toolchain provider."""
def __init__(
self,
name: str,
description: str,
probe_fn,
env_fn,
):
self.name = name
self.description = description
self.probe = probe_fn # (config) -> Dict[str, Any]
self.build_env = env_fn # (config) -> Dict[str, str]
def register_provider(spec: ProviderSpec) -> None:
"""Register a toolchain provider."""
_PROVIDERS[spec.name] = spec
def get_provider(name: str) -> Optional[ProviderSpec]:
"""Look up a registered provider by name. Returns None if unknown."""
return _PROVIDERS.get(name)
def list_providers() -> List[Dict[str, str]]:
"""Return a list of all registered providers with name and description."""
return [
{"name": spec.name, "description": spec.description}
for spec in sorted(_PROVIDERS.values(), key=lambda s: s.name)
]
# ---------------------------------------------------------------------------
# Generic chroot toolchain detection
# ---------------------------------------------------------------------------
# Regex to detect cross-compiler triple prefixes in /usr/bin/.
# Matches patterns like: arm-linux-gnueabihf-gcc, mipsel-openwrt-linux-gcc,
# x86_64-pc-linux-gnu-gcc, etc.
_CROSS_GCC_RE = re.compile(
r"^(.+?)-gcc$"
)
# Known cross-compiler triple patterns and their arch family.
# Used to classify detected cross-compilers by architecture.
_TRIPLE_ARCH_MAP: Dict[str, str] = {
"arm": "arm",
"aarch64": "arm64",
"mipsel": "mipsel",
"mips": "mips",
"x86_64": "x86_64",
"i686": "x86_64",
"tilegx": "tilegx",
"riscv64": "riscv64",
}
def _detect_cross_compilers(chroot_path: str) -> List[Dict[str, str]]:
"""Scan a chroot's /usr/bin/ for cross-compiler triples.
Returns a list of dicts with keys: triple, cc, cxx, arch, prefix.
Sorted by triple for deterministic ordering.
"""
bin_dir = os.path.join(chroot_path, "usr", "bin")
if not os.path.isdir(bin_dir):
return []
compilers = []
for entry in os.listdir(bin_dir):
m = _CROSS_GCC_RE.match(entry)
if not m:
continue
triple = m.group(1)
gcc_path = os.path.join(bin_dir, entry)
gxx_path = os.path.join(bin_dir, f"{triple}-g++")
if not os.path.isfile(gcc_path):
continue
# Classify the arch from the triple's first component.
arch = "unknown"
for prefix, family in _TRIPLE_ARCH_MAP.items():
if triple.startswith(prefix + "-") or triple.startswith(prefix + "_"):
arch = family
break
compilers.append({
"triple": triple,
"cc": gcc_path,
"cxx": gxx_path if os.path.isfile(gxx_path) else gcc_path,
"arch": arch,
"prefix": os.path.join(bin_dir, f"{triple}-"),
})
compilers.sort(key=lambda c: c["triple"])
return compilers
def _chroot_native_env(chroot_path: str, config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment for a native chroot toolchain.
Sets CC/CXX to the chroot's gcc/g++, adds the chroot's
/usr/bin and /bin to PATH, and points -I/-L at the chroot's
sysroot directories.
"""
env: Dict[str, str] = {}
# Prefer the chroot's compiler over the host's.
chroot_gcc = os.path.join(chroot_path, "usr", "bin", "gcc")
chroot_gxx = os.path.join(chroot_path, "usr", "bin", "g++")
if os.path.isfile(chroot_gcc):
env["CC"] = chroot_gcc
else:
env["CC"] = "gcc"
if os.path.isfile(chroot_gxx):
env["CXX"] = chroot_gxx
else:
env["CXX"] = "g++"
# Point include and library paths at the chroot.
sysroot_include = os.path.join(chroot_path, "usr", "include")
sysroot_lib = os.path.join(chroot_path, "usr", "lib")
include_flags = f" -I{sysroot_include}" if os.path.isdir(sysroot_include) else ""
lib_flags = f" -L{sysroot_lib}" if os.path.isdir(sysroot_lib) else ""
# User-supplied flags from config.
extra_cflags = config.get("cflags", "")
extra_ldflags = config.get("ldflags", "")
env["CFLAGS"] = f"-O2 -pipe{include_flags} {extra_cflags}".rstrip()
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = f"-Wl,-O1 -Wl,--as-needed{lib_flags} {extra_ldflags}".rstrip()
# Add chroot paths to PATH so the executor can find tools.
chroot_bin = os.path.join(chroot_path, "usr", "bin")
chroot_sbin = os.path.join(chroot_path, "usr", "sbin")
host_path = os.environ.get("PATH", "/usr/bin:/bin")
env["PATH"] = f"{chroot_bin}:{chroot_sbin}:{host_path}"
env["FESTER_CHROOT"] = chroot_path
env["toolchain_source"] = "chroot"
return env
def _chroot_cross_env(
chroot_path: str,
compiler: Dict[str, str],
config: Dict[str, Any],
) -> Dict[str, str]:
"""Build environment for a cross-compiler inside a chroot.
Uses the detected cross-compiler triple prefix for CC/CXX,
and sets sysroot flags to the chroot's include/lib directories.
"""
env: Dict[str, str] = {}
env["CC"] = compiler["cc"]
env["CXX"] = compiler["cxx"]
triple = compiler["triple"]
# Sysroot paths from the chroot.
sysroot_include = os.path.join(chroot_path, "usr", "include")
sysroot_lib = os.path.join(chroot_path, "usr", "lib")
sysroot = config.get("sysroot", chroot_path)
sysroot_flags = f" --sysroot={sysroot}" if os.path.isdir(sysroot) else ""
include_flags = f" -I{sysroot_include}" if os.path.isdir(sysroot_include) and not sysroot_flags else ""
lib_flags = f" -L{sysroot_lib}" if os.path.isdir(sysroot_lib) and not sysroot_flags else ""
# User-supplied flags.
extra_cflags = config.get("cflags", "")
extra_ldflags = config.get("ldflags", "")
march = config.get("march", "")
march_flag = f" -march={march}" if march else ""
env["CFLAGS"] = f"-O2{march_flag}{sysroot_flags}{include_flags} -pipe {extra_cflags}".rstrip()
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = f"-Wl,-O1 -Wl,--as-needed{sysroot_flags}{lib_flags} {extra_ldflags}".rstrip()
# Cross-compile metadata for executors.
env["FESTER_CHROOT"] = chroot_path
env["FESTER_CROSS_TRIPLE"] = triple
env["FESTER_CROSS_ARCH"] = compiler["arch"]
env["toolchain_source"] = "chroot-cross"
return env
# ---------------------------------------------------------------------------
# Provider implementations
# ---------------------------------------------------------------------------
# --- Generic chroot provider ---
def _probe_chroot(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a generic distro chroot for available toolchains."""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "chroot path not found"}
# Check for essential chroot structure.
has_bin = os.path.isdir(os.path.join(path, "usr", "bin"))
has_lib = os.path.isdir(os.path.join(path, "usr", "lib"))
if not has_bin or not has_lib:
return {"available": False, "reason": "chroot missing usr/bin or usr/lib"}
# Detect cross-compilers.
cross = _detect_cross_compilers(path)
has_native = os.path.isfile(os.path.join(path, "usr", "bin", "gcc"))
return {
"available": has_native or len(cross) > 0,
"path": path,
"has_native": has_native,
"cross_compilers": cross,
"provider": "chroot",
}
def _env_chroot(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a generic chroot."""
probe = _probe_chroot(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
path = probe["path"]
# If a specific cross target is requested, use it.
target_arch = config.get("target_arch", "")
if target_arch and probe["cross_compilers"]:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
return _chroot_cross_env(path, cc, config)
# If there's exactly one cross-compiler, use it.
if len(probe["cross_compilers"]) == 1 and not probe["has_native"]:
return _chroot_cross_env(path, probe["cross_compilers"][0], config)
# Default to native.
return _chroot_native_env(path, config)
# --- Buildroot provider ---
def _probe_buildroot(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a Buildroot output tree for cross-compilers.
Buildroot layout (standard):
output/host/ — host cross-compiler (bin/, lib/, usr/)
output/staging/ — target sysroot (usr/include, usr/lib)
output/target/ — root filesystem image
"""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "buildroot path not found"}
# Detect standard Buildroot layout.
host_dir = os.path.join(path, "output", "host")
staging_dir = os.path.join(path, "output", "staging")
if not os.path.isdir(host_dir):
return {"available": False, "reason": "buildroot output/host/ not found"}
# Scan for cross-compilers in output/host/bin/.
cross = _detect_cross_compilers(host_dir)
return {
"available": len(cross) > 0,
"path": path,
"host_dir": host_dir,
"staging_dir": staging_dir,
"cross_compilers": cross,
"provider": "buildroot",
}
def _env_buildroot(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a Buildroot SDK."""
probe = _probe_buildroot(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
host_dir = probe["host_dir"]
staging_dir = probe["staging_dir"]
# Select cross-compiler.
target_arch = config.get("target_arch", "")
compiler = None
if target_arch:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
compiler = cc
break
if compiler is None and probe["cross_compilers"]:
compiler = probe["cross_compilers"][0]
if compiler is None:
return {"toolchain_source": "gcc"}
env: Dict[str, str] = {}
env["CC"] = compiler["cc"]
env["CXX"] = compiler["cxx"]
# Buildroot uses staging/ as the sysroot for target headers/libs.
sysroot = staging_dir if os.path.isdir(staging_dir) else host_dir
sysroot_flag = f" --sysroot={sysroot}"
# Buildroot host/bin also contains pkg-config and other tools.
host_bin = os.path.join(host_dir, "bin")
host_path = os.environ.get("PATH", "/usr/bin:/bin")
march = config.get("march", "")
march_flag = f" -march={march}" if march else ""
env["CFLAGS"] = f"-O2{march_flag}{sysroot_flag} -pipe"
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = f"-Wl,-O1 -Wl,--as-needed{sysroot_flag}"
env["PATH"] = f"{host_bin}:{host_path}"
env["PKG_CONFIG_SYSROOT_DIR"] = sysroot
env["PKG_CONFIG_PATH"] = os.path.join(sysroot, "usr", "lib", "pkgconfig")
env["FESTER_BUILDROOT"] = probe["path"]
env["FESTER_CROSS_TRIPLE"] = compiler["triple"]
env["FESTER_CROSS_ARCH"] = compiler["arch"]
env["toolchain_source"] = "buildroot"
return env
# --- SourceMage chroot provider ---
def _probe_sourcemage(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a SourceMage GL test chroot.
SourceMage chroots contain the standard GNU toolchain plus the
sorcery spell system. The chroot is typically created via:
sorcery cast chroot
# or: smgl-chroot create /var/lib/smgl-chroot
Detection: look for /var/lib/sorcery inside the chroot path.
"""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "sourcemage chroot path not found"}
# SourceMage marker: sorcery config directory.
smgl_marker = os.path.join(path, "var", "lib", "sorcery")
if not os.path.isdir(smgl_marker):
return {"available": False, "reason": "not a SourceMage chroot (no var/lib/sorcery)"}
has_gcc = os.path.isfile(os.path.join(path, "usr", "bin", "gcc"))
if not has_gcc:
return {"available": False, "reason": "sourcemage chroot missing gcc"}
cross = _detect_cross_compilers(path)
return {
"available": True,
"path": path,
"has_native": True,
"cross_compilers": cross,
"provider": "sourcemage",
}
def _env_sourcemage(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a SourceMage chroot."""
probe = _probe_sourcemage(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
path = probe["path"]
# SourceMage supports cross-compiles via sorcery's CROSS_COMPILE
# variable. If a cross-compiler is present, use it.
target_arch = config.get("target_arch", "")
if target_arch and probe["cross_compilers"]:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
return _chroot_cross_env(path, cc, config)
env = _chroot_native_env(path, config)
env["toolchain_source"] = "sourcemage"
# SourceMage-specific: expose sorcery inside the chroot.
env["FESTER_SOURCEMAGE"] = path
sorcery_bin = os.path.join(path, "usr", "sbin")
if os.path.isdir(sorcery_bin):
env["PATH"] = f"{sorcery_bin}:{env.get('PATH', '')}"
return env
# --- Gentoo stage3 chroot provider ---
def _probe_gentoo(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a Gentoo stage3 chroot.
Gentoo chroots are typically unpacked from a stage3 tarball at
a path like /var/lib/gentoo-chroot/. Detection: look for
/etc/gentoo-release inside the chroot.
Gentoo's cross-compilation is handled by crossdev, which installs
cross-compilers to /usr/<triple>/gcc-bin/<version>/.
"""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "gentoo chroot path not found"}
gentoo_marker = os.path.join(path, "etc", "gentoo-release")
if not os.path.isfile(gentoo_marker):
return {"available": False, "reason": "not a Gentoo chroot (no etc/gentoo-release)"}
has_gcc = os.path.isfile(os.path.join(path, "usr", "bin", "gcc"))
if not has_gcc:
return {"available": False, "reason": "gentoo chroot missing gcc"}
# Gentoo crossdev installs to /usr/<triple>/gcc-bin/<version>/
cross = _detect_cross_compilers(path)
# Also check Gentoo's crossdev-specific layout.
usr_contents = os.listdir(os.path.join(path, "usr")) if os.path.isdir(os.path.join(path, "usr")) else []
for entry in usr_contents:
crossdev_bin = os.path.join(path, "usr", entry, "gcc-bin")
if os.path.isdir(crossdev_bin):
for ver in os.listdir(crossdev_bin):
ver_dir = os.path.join(crossdev_bin, ver)
if os.path.isdir(ver_dir):
for tool in os.listdir(ver_dir):
if tool.endswith("-gcc"):
triple = tool[:-4]
gcc_path = os.path.join(ver_dir, tool)
gxx_name = tool.replace("-gcc", "-g++")
gxx_path = os.path.join(ver_dir, gxx_name)
arch = "unknown"
for prefix, family in _TRIPLE_ARCH_MAP.items():
if triple.startswith(prefix + "-") or triple.startswith(prefix + "_"):
arch = family
break
cross.append({
"triple": triple,
"cc": gcc_path,
"cxx": gxx_path if os.path.isfile(gxx_path) else gcc_path,
"arch": arch,
"prefix": os.path.join(ver_dir, f"{triple}-"),
})
# Deduplicate by triple.
seen = set()
deduped = []
for cc in sorted(cross, key=lambda c: c["triple"]):
if cc["triple"] not in seen:
seen.add(cc["triple"])
deduped.append(cc)
return {
"available": True,
"path": path,
"has_native": True,
"cross_compilers": deduped,
"provider": "gentoo",
}
def _env_gentoo(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a Gentoo stage3 chroot."""
probe = _probe_gentoo(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
path = probe["path"]
target_arch = config.get("target_arch", "")
if target_arch and probe["cross_compilers"]:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
return _chroot_cross_env(path, cc, config)
env = _chroot_native_env(path, config)
env["toolchain_source"] = "gentoo"
env["FESTER_GENTOO"] = path
# Gentoo-specific: expose portage.
portage_bin = os.path.join(path, "usr", "sbin")
if os.path.isdir(portage_bin):
env["PATH"] = f"{portage_bin}:{env.get('PATH', '')}"
return env
# --- LEDE / OpenWrt SDK provider ---
def _probe_lede(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a LEDE/OpenWrt SDK for cross-compilers.
LEDE/OpenWrt SDK layout:
staging_dir/ — target sysroot
staging_dir/toolchain/ — cross-compiler (bin/, lib/, include/)
staging_dir/target/ — target root filesystem
The cross-compiler triple is typically mipsel-openwrt-linux-* or
arm-openwrt-linux-*.
"""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "lede/openwrt path not found"}
# Detect LEDE/OpenWrt SDK markers.
staging_dir = None
for candidate in [
os.path.join(path, "staging_dir"),
os.path.join(path, "staging_dir", "toolchain"),
os.path.join(path, "openwrt", "staging_dir"),
]:
if os.path.isdir(candidate):
staging_dir = candidate
break
if staging_dir is None:
return {"available": False, "reason": "no staging_dir/ found (not a LEDE/OpenWrt SDK)"}
# LEDE cross-compilers are typically in staging_dir/toolchain/bin/
toolchain_bin = os.path.join(staging_dir, "bin")
if not os.path.isdir(toolchain_bin):
# Try staging_dir/toolchain/bin/ as a subdirectory
toolchain_bin = os.path.join(path, "staging_dir", "toolchain", "bin")
cross = _detect_cross_compilers(staging_dir) if os.path.isdir(staging_dir) else []
# Also scan toolchain/bin specifically.
if os.path.isdir(toolchain_bin):
for entry in os.listdir(toolchain_bin):
m = _CROSS_GCC_RE.match(entry)
if not m:
continue
triple = m.group(1)
gcc_path = os.path.join(toolchain_bin, entry)
if os.path.isfile(gcc_path):
# Check for duplicates.
existing = [c for c in cross if c["triple"] == triple]
if not existing:
arch = "unknown"
for prefix, family in _TRIPLE_ARCH_MAP.items():
if triple.startswith(prefix + "-") or triple.startswith(prefix + "_"):
arch = family
break
cross.append({
"triple": triple,
"cc": gcc_path,
"cxx": os.path.join(toolchain_bin, f"{triple}-g++"),
"arch": arch,
"prefix": os.path.join(toolchain_bin, f"{triple}-"),
})
return {
"available": len(cross) > 0,
"path": path,
"staging_dir": staging_dir,
"cross_compilers": cross,
"provider": "lede",
}
def _env_lede(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a LEDE/OpenWrt SDK."""
probe = _probe_lede(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
target_arch = config.get("target_arch", "")
compiler = None
if target_arch:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
compiler = cc
break
if compiler is None and probe["cross_compilers"]:
compiler = probe["cross_compilers"][0]
if compiler is None:
return {"toolchain_source": "gcc"}
staging_dir = probe["staging_dir"]
env: Dict[str, str] = {}
env["CC"] = compiler["cc"]
env["CXX"] = compiler["cxx"]
# LEDE sysroot is staging_dir/target/ or staging_dir/ itself.
target_dir = os.path.join(staging_dir, "target")
sysroot = target_dir if os.path.isdir(target_dir) else staging_dir
sysroot_flag = f" --sysroot={sysroot}"
# LEDE toolchain/bin/ has wrapper scripts that need to be on PATH.
toolchain_bin = os.path.join(staging_dir, "bin")
host_path = os.environ.get("PATH", "/usr/bin:/bin")
env["CFLAGS"] = f"-O2{sysroot_flag} -pipe"
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = f"-Wl,-O1 -Wl,--as-needed{sysroot_flag}"
env["PATH"] = f"{toolchain_bin}:{host_path}"
env["FESTER_LEDE"] = probe["path"]
env["FESTER_CROSS_TRIPLE"] = compiler["triple"]
env["FESTER_CROSS_ARCH"] = compiler["arch"]
env["toolchain_source"] = "lede"
return env
# --- Lunar Linux chroot provider ---
def _probe_lunar(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe a Lunar Linux chroot.
Lunar Linux uses the moonbase package system. Chroots are
typically at /var/lib/lunar-chroot/. Detection: look for
/var/lib/lunar/ inside the chroot path.
"""
path = config.get("path", "")
if not path or not os.path.isdir(path):
return {"available": False, "reason": "lunar chroot path not found"}
lunar_marker = os.path.join(path, "var", "lib", "lunar")
if not os.path.isdir(lunar_marker):
return {"available": False, "reason": "not a Lunar Linux chroot (no var/lib/lunar)"}
has_gcc = os.path.isfile(os.path.join(path, "usr", "bin", "gcc"))
if not has_gcc:
return {"available": False, "reason": "lunar chroot missing gcc"}
cross = _detect_cross_compilers(path)
return {
"available": True,
"path": path,
"has_native": True,
"cross_compilers": cross,
"provider": "lunar",
}
def _env_lunar(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment from a Lunar Linux chroot."""
probe = _probe_lunar(config)
if not probe["available"]:
return {"toolchain_source": "gcc"}
path = probe["path"]
target_arch = config.get("target_arch", "")
if target_arch and probe["cross_compilers"]:
for cc in probe["cross_compilers"]:
if cc["arch"] == target_arch:
return _chroot_cross_env(path, cc, config)
env = _chroot_native_env(path, config)
env["toolchain_source"] = "lunar"
env["FESTER_LUNAR"] = path
# Lunar-specific: expose moonbase tools.
lunar_bin = os.path.join(path, "usr", "sbin")
if os.path.isdir(lunar_bin):
env["PATH"] = f"{lunar_bin}:{env.get('PATH', '')}"
return env
# --- Native (host) provider ---
def _probe_native(config: Dict[str, Any]) -> Dict[str, Any]:
"""Probe the host system's native toolchain.
Always available as a fallback. Uses the host's gcc/g++ directly.
"""
has_gcc = shutil.which("gcc") is not None
return {
"available": has_gcc,
"provider": "native",
"has_native": has_gcc,
"cross_compilers": _detect_cross_compilers("/"),
}
def _env_native(config: Dict[str, Any]) -> Dict[str, str]:
"""Build environment using the host's native toolchain.
This is the no-chroot, no-BTC, no-Buildroot fallback. Just
ccache + distcc + the host gcc.
"""
env: Dict[str, str] = {}
env["CC"] = os.environ.get("CC", "ccache gcc")
env["CXX"] = os.environ.get("CXX", "ccache g++")
env["CFLAGS"] = os.environ.get("CFLAGS", "-O2 -pipe")
env["CXXFLAGS"] = os.environ.get("CXXFLAGS", env["CFLAGS"])
env["LDFLAGS"] = os.environ.get("LDFLAGS", "-Wl,-O1 -Wl,--as-needed")
env["toolchain_source"] = "native"
# If the host has cross-compilers (e.g., from apt install gcc-arm-linux-gnueabihf),
# detect and expose them.
target_arch = config.get("target_arch", "")
if target_arch:
host_cross = _detect_cross_compilers("/")
for cc in host_cross:
if cc["arch"] == target_arch:
env["CC"] = cc["cc"]
env["CXX"] = cc["cxx"]
env["FESTER_CROSS_TRIPLE"] = cc["triple"]
env["FESTER_CROSS_ARCH"] = cc["arch"]
env["toolchain_source"] = "native-cross"
break
return env
# ---------------------------------------------------------------------------
# Register all providers
# ---------------------------------------------------------------------------
register_provider(ProviderSpec(
name="native",
description="Host system toolchain (gcc/g++ with ccache/distcc)",
probe_fn=_probe_native,
env_fn=_env_native,
))
register_provider(ProviderSpec(
name="buildroot",
description="Buildroot SDK cross-compiler (output/host/ + staging/)",
probe_fn=_probe_buildroot,
env_fn=_env_buildroot,
))
register_provider(ProviderSpec(
name="sourcemage",
description="SourceMage GL chroot (sorcery spell system)",
probe_fn=_probe_sourcemage,
env_fn=_env_sourcemage,
))
register_provider(ProviderSpec(
name="gentoo",
description="Gentoo stage3 chroot (portage + crossdev)",
probe_fn=_probe_gentoo,
env_fn=_env_gentoo,
))
register_provider(ProviderSpec(
name="lede",
description="LEDE/OpenWrt SDK (mipsel/arm router toolchains)",
probe_fn=_probe_lede,
env_fn=_env_lede,
))
register_provider(ProviderSpec(
name="lunar",
description="Lunar Linux chroot (moonbase package system)",
probe_fn=_probe_lunar,
env_fn=_env_lunar,
))
register_provider(ProviderSpec(
name="chroot",
description="Generic distro chroot (auto-detect toolchain)",
probe_fn=_probe_chroot,
env_fn=_env_chroot,
))