822 lines
28 KiB
Python
Executable File
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,
|
|
)) |