fester/backend/toolchain/btc.py

805 lines
29 KiB
Python
Executable File

"""BTC.sh (Build Tool Chain) integration for Fester.
BTC.sh produces a sovereign, forensically-stamped GCC toolchain with:
- .note.BTC ELF note section (Silicon Birth Certificate)
- xattr identity stamps (user.btc.identity, user.btc.hash)
- Separated debug symbols with GNU debug links
- Thermal + entropy sentinel during the forge process
- Multi-architecture cross-compilation support (BTC-0.4.0+)
Supported target families (BTC-0.4.0+):
x86_64: haswell, haswell-ep, skylake, skylake-x, skylake-server,
znver1, znver2, znver3, znver4,
apu-zn1, apu-zn2, apu-zn3, apu-zn4,
atom-silvermont, atom-goldmont, atom-tremont, atom-sierraforest
mipsel: mipselr2 (MIPS32R2 LE, o32 ABI, musl)
arm: armv7 (Cortex-A NEON hard-float, musl)
tilegx: tilegx (Tilera TILE-Gx72, musl)
This module allows Fester to:
1. Detect BTC golden images on nodes (via manifest JSON or filename)
2. Use BTC as a cross-compiler for the node's configured target
3. Verify BTC forensic stamps on build outputs
4. Pass BTC-aware build environments to executors
5. Track BTC SYS_LABEL for cross-node provenance
"""
import hashlib
import json
import logging
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# SYS_LABEL parsing
# ---------------------------------------------------------------------------
# BTC 0.3.x native format: DCOSNET-{MICROARCH}-{ISA}-{OPT}
# Example: DCOSNET-HASWELL-AVX2-LTO
# BTC 0.4.0 cross format: DCOSNET-{FAMILY}-{TARGET_ID}-{ISA}-CROSS
# Example: DCOSNET-MIPS-MIPSELR2-MIPS32-CROSS
# Unified regex: both formats have 3 or 4 uppercase segments separated by "-"
_SYS_LABEL_RE = re.compile(
r"^DCOSNET-([A-Z0-9]+)(?:-([A-Z0-9]+))?-([A-Z0-9]+)(?:-([A-Z0-9]+))?$"
)
# Golden image filename pattern
_GOLDEN_RE = re.compile(r"^(.+)-toolchain-golden\.tar\.xz$")
# Manifest filename pattern: {SYS_LABEL}-manifest.json
_MANIFEST_RE = re.compile(r"^(.+)-manifest\.json$")
# .note.BTC stamp field parser
_NOTE_FIELD_RE = re.compile(r"(\w+):\s*([^\|]+)")
# Maps readelf note field keys (lowercase) to result dict keys.
# Unified lookup avoids a long if/elif chain (PEP 868 / SEI CERT).
_NOTE_KEY_MAP = {
"org": "org",
"k": "kernel",
"kernel": "kernel",
"arch": "arch",
"label": "label",
"forge": "forge",
}
# ISA flag table — maps ISA tag (uppercase) to compiler flags.
# Table-driven lookup replaces if/elif chains (PEP 868).
_ISA_FLAGS: Dict[str, str] = {
"AVX2": " -mavx2",
"AVX512": " -mavx512f -mavx512dq -mavx512vl -mavx512bw",
"SSE4_2": " -msse4.2",
"NEON": " -mfpu=neon -mfloat-abi=hard",
"MIPS32": "",
"TILE": "",
}
# Known x86_64 microarchitecture march values — these can be passed
# directly to -march=. Non-x86 targets use their target_march from
# the manifest instead.
_KNOWN_MARCH = {
"haswell", "broadwell", "sandybridge", "ivybridge", "skylake",
"skylake-avx512", "skylake-server", "znver1", "znver2", "znver3",
"znver4", "haswell-ep",
}
def _parse_sys_label(label: str) -> Dict[str, str]:
"""Parse a SYS_LABEL into its component parts.
BTC 0.3.x: DCOSNET-HASWELL-AVX2-LTO
-> family=HASWELL, target_id=AVX2, isa=LTO, opt=""
BTC 0.4.0 cross: DCOSNET-MIPS-MIPSELR2-MIPS32-CROSS
-> family=MIPS, target_id=MIPSELR2, isa=MIPS32, opt=CROSS
BTC 0.4.0 native: DCOSNET-HASWELL-AVX2-LTO (unchanged)
-> family=HASWELL, target_id="", isa=AVX2, opt=LTO
Returns a dict with keys: full, family, target_id, isa, opt, cross.
"""
m = _SYS_LABEL_RE.match(label)
if not m:
return {"full": label}
groups = m.groups()
result: Dict[str, str] = {"full": label}
if len(groups) == 4 and groups[3] == "CROSS":
# 4-segment: DCOSNET-FAMILY-TARGET_ID-ISA-CROSS
result["family"] = groups[0].lower()
result["target_id"] = groups[1].lower()
result["isa"] = groups[2].lower()
result["opt"] = ""
result["cross"] = "1"
elif groups[1] in ("AVX2", "AVX512", "SSE4_2", "NEON", "MIPS32", "TILE", "LTO"):
# 3-segment native: DCOSNET-MICROARCH-ISA-OPT
# The first group IS the microarch/target_id
result["family"] = groups[0].lower()
result["target_id"] = groups[0].lower()
result["isa"] = groups[1].lower()
result["opt"] = groups[2].lower() if groups[2] else ""
result["cross"] = "0"
else:
# Fallback: treat first two as family + target_id
result["family"] = groups[0].lower()
result["target_id"] = groups[1].lower() if groups[1] else groups[0].lower()
result["isa"] = groups[2].lower() if groups[2] else ""
result["opt"] = groups[3].lower() if groups[3] else ""
result["cross"] = "0"
return result
def _run(cmd: list, timeout: int = 30) -> Tuple[int, str]:
"""Run a subprocess command, returning (rc, stdout+stderr).
Any exception is caught and returned as rc=-1.
"""
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
)
return proc.returncode, proc.stdout + proc.stderr
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
logger.debug("btc: command failed: %s -- %s", cmd, exc)
return -1, ""
def _load_manifest(btc_root: str, sys_label: str) -> Optional[Dict[str, Any]]:
"""Try to load the BTC manifest JSON for a given SYS_LABEL.
BTC 0.4.0+ writes a {SYS_LABEL}-manifest.json sidecar alongside the
golden image. This contains structured metadata (target_id, arch,
clib, triple, cflags, etc.) that is more reliable than parsing
filenames.
Returns the parsed JSON dict, or None if not found.
"""
manifest_path = Path(btc_root) / f"{sys_label}-manifest.json"
if not manifest_path.is_file():
return None
try:
with open(manifest_path, "r") as f:
return json.load(f)
except (OSError, json.JSONDecodeError) as exc:
logger.warning("btc: failed to load manifest %s: %s", manifest_path, exc)
return None
# ---------------------------------------------------------------------------
# 1. probe_btc -- detect BTC on a node
# ---------------------------------------------------------------------------
def probe_btc(
btc_root: str = "/opt/BTC",
target_filter: Optional[str] = None,
) -> Dict[str, Any]:
"""Check whether BTC is available on this node.
Looks for ``*-toolchain-golden.tar.xz`` inside *btc_root*, parses
the SYS_LABEL from the filename, and checks for the BTC.sh script.
If a *target_filter* is given, only matches a golden image whose
SYS_LABEL contains the filter string (useful for selecting a
specific cross-compiler among multiple installed targets).
If a manifest JSON is found, its structured fields are merged into
the result (target_id, arch, clib, triple, cross_mode, etc.).
Returns a dict:
available (bool) - True if a golden image is found
sys_label (str) - parsed SYS_LABEL, or empty string
golden_image (str) - absolute path of the golden tar.xz, or None
btc_path (str) - absolute path of BTC.sh, or None
arch (str) - base architecture (x86_64, arm, mipsel, tilegx)
family (str) - architecture family from SYS_LABEL
target_id (str) - BTC target identifier (haswell, znver3, armv7, etc.)
isa (str) - ISA tag (avx2, avx512, neon, mips32, tile)
opt (str) - optimisation tag (lto)
cross (str) - "1" for cross-compile, "0" for native
clib (str) - C library (glibc or musl), from manifest
triple (str) - GCC target triple, from manifest
target_march (str) - -march value, from manifest
manifest (dict) - full manifest JSON contents, or None
"""
root = Path(btc_root)
result: Dict[str, Any] = {
"available": False,
"sys_label": "",
"golden_image": None,
"btc_path": None,
"arch": "",
"family": "",
"target_id": "",
"isa": "",
"opt": "",
"cross": "0",
"clib": "",
"triple": "",
"target_march": "",
"manifest": None,
}
# Locate golden image(s) — if multiple exist, prefer one matching target_filter
golden = None
if root.is_dir():
for entry in sorted(root.iterdir()):
m = _GOLDEN_RE.match(entry.name)
if not m:
continue
label = entry.name[: -len("-toolchain-golden.tar.xz")]
if target_filter:
# Case-insensitive substring match against the full label
if target_filter.lower() not in label.lower():
continue
golden = entry
break
if golden is None:
logger.debug("btc: no golden image found in %s", btc_root)
return result
result["golden_image"] = str(golden.resolve())
# Parse SYS_LABEL from filename prefix
label = golden.name[: -len("-toolchain-golden.tar.xz")]
parsed = _parse_sys_label(label)
result["sys_label"] = parsed["full"]
result["family"] = parsed.get("family", "")
result["target_id"] = parsed.get("target_id", "")
result["isa"] = parsed.get("isa", "")
result["opt"] = parsed.get("opt", "")
result["cross"] = parsed.get("cross", "0")
# For backward compat: "arch" used to hold the SYS_LABEL arch segment
result["arch"] = parsed.get("family", "")
# Try to load the manifest for richer metadata
manifest = _load_manifest(btc_root, parsed["full"])
if manifest:
result["manifest"] = manifest
result["clib"] = manifest.get("clib", "")
result["triple"] = manifest.get("target_triple", "")
result["target_march"] = manifest.get("target_march", "")
result["target_id"] = manifest.get("target_id", result["target_id"])
result["arch"] = manifest.get("target_arch", result["arch"])
result["cross"] = str(manifest.get("cross_mode", result["cross"]))
# Check for BTC.sh in common locations
for candidate in [root / "BTC.sh", Path("/usr/local/bin/BTC.sh")]:
if candidate.is_file():
result["btc_path"] = str(candidate.resolve())
break
if result["btc_path"]:
result["available"] = True
elif result.get("golden_image"):
# Golden image exists but BTC.sh is not installed — BTC toolchain
# is usable for builds but cannot be re-forged.
result["available"] = True
logger.warning("btc: golden image found but BTC.sh is missing; "
"toolchain is usable but re-forge is not available")
else:
logger.debug("btc: no golden image or BTC.sh found")
return result
# ---------------------------------------------------------------------------
# 2. btc_build_env -- produce a BTC-aware environment dict
# ---------------------------------------------------------------------------
def _btc_sysroot(btc_root: str, sys_label: str) -> str:
"""Return the expected extracted sysroot path for a given SYS_LABEL.
BTC.sh extracts the toolchain into the cleanroom during the forge,
then packages it. After extraction, the sysroot lives at
``{btc_root}/{SYS_LABEL}-cleanroom/`` or wherever the operator
extracted the golden image.
"""
# Try the cleanroom path first (where BTC.sh builds it), then
# a simple extraction path.
cleanroom = os.path.join("/usr/src", f"{sys_label}-cleanroom")
if os.path.isdir(cleanroom):
return cleanroom
return os.path.join(btc_root, sys_label)
def btc_build_env(
project: Dict[str, Any],
node_config: Dict[str, Any],
btc_root: str = "/opt/BTC",
target: Optional[str] = None,
) -> Dict[str, str]:
"""Return a BTC-aware build environment dictionary.
If BTC is available on the node, CC/CXX point at the BTC
cross-compiler inside the extracted sysroot and optimisation
flags are derived from the SYS_LABEL or manifest metadata.
If BTC is not available, falls back to standard ``ccache gcc`` / ``ccache g++``.
For cross-compiled toolchains (BTC 0.4.0+), the compiler is named
``{triple}-gcc`` and ``{triple}-g++`` in the sysroot bin/ directory.
The *target* parameter allows selecting a specific cross-compiler
by target ID (e.g., "armv7") when multiple golden images exist.
The returned env is intended to be merged *on top of* the
ccache/distcc env produced by :func:`build_ccache_env`.
"""
probe = probe_btc(btc_root, target_filter=target)
env: Dict[str, str] = {}
if probe["available"]:
sysroot = _btc_sysroot(btc_root, probe["sys_label"])
target_id = probe.get("target_id", "") or "native"
triple = probe.get("triple", "")
target_march = probe.get("target_march", "") or target_id
# Determine the compiler binary names.
# Cross-toolchains (BTC 0.4.0+) use {triple}-gcc naming.
# Native toolchains use plain gcc/g++.
bin_dir = os.path.join(sysroot, "bin")
if os.path.isdir(bin_dir) and triple:
gcc_path = os.path.join(bin_dir, f"{triple}-gcc")
gxx_path = os.path.join(bin_dir, f"{triple}-g++")
if not os.path.isfile(gcc_path):
gcc_path = os.path.join(bin_dir, "gcc")
if not os.path.isfile(gxx_path):
gxx_path = os.path.join(bin_dir, "g++")
elif os.path.isdir(bin_dir):
gcc_path = os.path.join(bin_dir, "gcc")
gxx_path = os.path.join(bin_dir, "g++")
if not os.path.isfile(gcc_path):
gcc_path = "gcc"
if not os.path.isfile(gxx_path):
gxx_path = "g++"
else:
gcc_path = "gcc"
gxx_path = "g++"
env["CC"] = gcc_path
env["CXX"] = gxx_path
# Select the correct march value.
# For x86_64 microarchs, use the target_id or target_march directly.
# For non-x86 targets, the manifest provides the correct -march value.
march = target_march
if march in _KNOWN_MARCH:
pass # Already a valid -march= value
elif probe.get("target_march"):
march = probe["target_march"]
else:
march = target_id
# ISA-specific flags (table-driven lookup, PEP 868)
isa_upper = probe.get("isa", "").upper()
isa_flags = _ISA_FLAGS.get(isa_upper, "")
# LTO flags
opt = probe.get("opt", "").lower()
lto_flags = " -flto -ffat-lto-objects" if opt == "lto" else ""
# sysroot flag
sysroot_flag = f" --sysroot={sysroot}" if os.path.isdir(sysroot) else ""
env["CFLAGS"] = f"-O3 -march={march}{isa_flags}{lto_flags}{sysroot_flag} -pipe"
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = f"-Wl,-O1 -Wl,--as-needed{lto_flags}{sysroot_flag}"
env["BTC_MODE"] = "1"
env["BTC_SYS_LABEL"] = probe["sys_label"]
env["BTC_TARGET_ID"] = target_id
env["BTC_CROSS"] = probe.get("cross", "0")
# Propagate C library info so executors can make informed decisions
# (e.g., linking against -lcrypt for glibc vs. built-in for musl).
clib = probe.get("clib", "")
if clib:
env["BTC_CLIB"] = clib
# For cross-compiles, also set the target triple so executors
# can use it for configure --host= and similar.
if triple and probe.get("cross") == "1":
env["BTC_TARGET_TRIPLE"] = triple
else:
# Fallback: standard ccache gcc
env["CC"] = "ccache gcc"
env["CXX"] = "ccache g++"
env["BTC_MODE"] = "0"
return env
# ---------------------------------------------------------------------------
# 3. verify_btc_stamp -- read forensic stamps from a binary
# ---------------------------------------------------------------------------
def verify_btc_stamp(binary_path: str) -> Dict[str, Any]:
"""Read BTC forensic stamps from a compiled binary.
Uses ``readelf -n`` to extract the ``.note.BTC`` section and
``getfattr`` to read extended attributes. Computes a SHA-256 of
the file and checks it against the ``user.btc.hash`` xattr.
Returns a dict with:
has_note (bool) - .note.BTC section was found
has_xattr (bool) - xattr stamps were found
org (str) - organisation field, or empty
kernel (str) - kernel version field, or empty
arch (str) - architecture field, or empty
label (str) - SYS_LABEL field, or empty
forge (str) - forge step field, or empty
identity (str) - user.btc.identity xattr value, or empty
hash (str) - user.btc.hash xattr value, or empty
valid (bool) - True if the computed SHA-256 matches the stamp
"""
result: Dict[str, Any] = {
"has_note": False,
"has_xattr": False,
"org": "",
"kernel": "",
"arch": "",
"label": "",
"forge": "",
"identity": "",
"hash": "",
"valid": False,
}
if not os.path.isfile(binary_path):
logger.debug("btc: verify: binary not found: %s", binary_path)
return result
# -- readelf -n: look for .note.BTC --
rc, out = _run(["readelf", "-n", binary_path])
if rc == 0:
# readelf may emit multiple note sections; find the one that
# contains BTC-specific fields.
for line in out.splitlines():
for m in _NOTE_FIELD_RE.finditer(line):
key, val = m.group(1).lower(), m.group(2).strip()
field = _NOTE_KEY_MAP.get(key)
if field is not None:
result[field] = val
result["has_note"] = True
# -- getfattr: read xattr stamps --
rc, out = _run(["getfattr", "-n", "user.btc.identity", binary_path])
if rc == 0:
# Output looks like: # file: /path/binary
# user.btc.identity="..."
for line in out.splitlines():
if line.strip().startswith("user.btc.identity="):
result["identity"] = line.split("=", 1)[1].strip('"')
result["has_xattr"] = True
rc, out = _run(["getfattr", "-n", "user.btc.hash", binary_path])
if rc == 0:
for line in out.splitlines():
if line.strip().startswith("user.btc.hash="):
result["hash"] = line.split("=", 1)[1].strip('"')
result["has_xattr"] = True
# -- SHA-256 integrity check --
if result["hash"]:
sha256 = hashlib.sha256()
try:
with open(binary_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
computed = sha256.hexdigest()
# Strip optional "sha256:" prefix for cross-project compatibility
# (sorcery-go writes bare hex, but future consumers may prefix).
stored_hash = result["hash"]
if stored_hash.startswith("sha256:"):
stored_hash = stored_hash[7:]
result["valid"] = computed == stored_hash
if not result["valid"]:
logger.warning(
"btc: hash mismatch for %s: computed=%s stamp=%s",
binary_path, computed, result["hash"],
)
except OSError as exc:
logger.warning("btc: could not hash %s: %s", binary_path, exc)
return result
# ---------------------------------------------------------------------------
# 4. apply_btc_stamp -- inject forensic stamps into a binary
# ---------------------------------------------------------------------------
def apply_btc_stamp(
binary_path: str,
forge_step: str,
sys_label: str,
kernel_ver: str = "7.1",
arch: str = "haswell",
) -> Dict[str, Any]:
"""Apply BTC forensic stamps to a compiled binary.
This creates a ``.note.BTC`` ELF note section via a temporary
assembly stub, sets extended attributes, and optionally separates
debug symbols (when ``BTC_STRIP_MODE=1``).
Errors are logged as warnings but never cause the build to fail --
stamping is a best-effort post-build enhancement.
Returns a dict:
success (bool) - True if the note section was injected
stripped (bool) - True if debug symbols were separated
error (str) - error message, or empty on success
"""
result: Dict[str, Any] = {
"success": False,
"stripped": False,
"error": "",
}
if not os.path.isfile(binary_path):
result["error"] = f"binary not found: {binary_path}"
logger.warning("btc: stamp: %s", result["error"])
return result
# Build the note payload string
stamp = (
f"Org: dcos.net|K:{kernel_ver}|"
f"Arch:{arch}|Label:{sys_label}|Forge:{forge_step}"
)
note_obj = None
try:
# -- Create a temporary .note.BTC object file via inline asm --
note_obj = _assemble_btc_note(stamp)
if note_obj is None:
result["error"] = "failed to assemble .note.BTC stub"
logger.warning("btc: stamp: %s", result["error"])
return result
# Inject the note section into the binary
tmp_binary = binary_path + ".btc.tmp"
rc, _ = _run([
"objcopy",
"--add-section", ".note.BTC=" + note_obj,
"--set-section-flags", ".note.BTC=alloc,readonly",
binary_path,
tmp_binary,
])
if rc != 0:
result["error"] = "objcopy --add-section failed"
logger.warning("btc: stamp: %s", result["error"])
return result
# Replace the original binary
shutil.move(tmp_binary, binary_path)
os.chmod(binary_path, os.stat(binary_path).st_mode | 0o111)
result["success"] = True
except OSError as exc:
result["error"] = str(exc)
logger.warning("btc: stamp: %s", result["error"])
return result
finally:
if note_obj and os.path.isfile(note_obj):
try:
os.unlink(note_obj)
except OSError:
pass
# -- Set extended attributes --
_set_btc_xattr(binary_path, sys_label, stamp)
# -- Optional debug symbol separation --
if os.environ.get("BTC_STRIP_MODE", "0") == "1":
result["stripped"] = _separate_debug_symbols(binary_path)
return result
def _assemble_btc_note(stamp: str) -> Optional[str]:
"""Assemble a small .note.BTC ELF note object and return its path.
The note is built as a temporary .s file compiled with ``gcc -c``.
Returns the path to the .o file, or None on failure.
"""
note_name = "BTC"
# n_namesz includes the null terminator; n_descsz is len(stamp)+1
note_asm = (
'.section .note.BTC, "a", @note\n'
'.balign 4\n'
'.long 4 /* namesz */\n'
f'.long {len(stamp) + 1} /* descsz */\n'
'.long 1 /* type (NT_VERSION) */\n'
'.asciz "BTC" /* name */\n'
f'.asciz "{stamp}" /* desc */\n'
'.balign 4\n'
)
tmp_s = None
tmp_o = None
try:
fd_s, tmp_s = tempfile.mkstemp(suffix=".s")
os.write(fd_s, note_asm.encode())
os.close(fd_s)
tmp_o = tmp_s.replace(".s", ".o")
rc, _ = _run(["gcc", "-c", tmp_s, "-o", tmp_o])
if rc == 0 and os.path.isfile(tmp_o):
return tmp_o
return None
except OSError:
return None
finally:
if tmp_s and os.path.isfile(tmp_s):
try:
os.unlink(tmp_s)
except OSError:
pass
def _set_btc_xattr(binary_path: str, sys_label: str, stamp: str) -> None:
"""Set BTC extended attributes on a binary.
Sets user.btc.identity to the SYS_LABEL and user.btc.hash to the
SHA-256 hex digest of the file contents.
"""
try:
# Compute SHA-256 of the file *before* setting xattrs (so the
# hash does not depend on the xattr data itself).
sha256 = hashlib.sha256()
with open(binary_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
file_hash = sha256.hexdigest()
_run(["setfattr", "-n", "user.btc.identity", "-v", sys_label, binary_path])
_run(["setfattr", "-n", "user.btc.hash", "-v", file_hash, binary_path])
except OSError as exc:
logger.warning("btc: failed to set xattr on %s: %s", binary_path, exc)
def _separate_debug_symbols(binary_path: str) -> bool:
"""Separate debug symbols from *binary_path*.
Uses the standard objcopy/strip/add-gnu-debuglink sequence:
1. objcopy --only-keep-debug -> {path}.debug
2. strip --strip-unneeded on the original
3. objcopy --add-gnu-debuglink={path}.debug
Returns True on success, False on any failure.
"""
debug_path = binary_path + ".debug"
try:
rc, _ = _run([
"objcopy", "--only-keep-debug", binary_path, debug_path,
])
if rc != 0:
return False
rc, _ = _run(["strip", "--strip-unneeded", binary_path])
if rc != 0:
return False
rc, _ = _run([
"objcopy", "--add-gnu-debuglink=" + debug_path, binary_path,
])
if rc != 0:
return False
return True
except OSError:
return False
# ---------------------------------------------------------------------------
# 5. list_btc_targets -- enumerate available golden images
# ---------------------------------------------------------------------------
def list_btc_targets(btc_root: str = "/opt/BTC") -> List[Dict[str, Any]]:
"""List all BTC golden images and their target metadata.
Scans *btc_root* for ``*-toolchain-golden.tar.xz`` files and
``*-manifest.json`` sidecars. Returns a list of dicts, one per
golden image, with parsed SYS_LABEL components and manifest data.
This is useful for the Fester API to show operators which
cross-compilers are available on each node.
"""
root = Path(btc_root)
targets: List[Dict[str, Any]] = []
if not root.is_dir():
return targets
for entry in sorted(root.iterdir()):
m = _GOLDEN_RE.match(entry.name)
if not m:
continue
label = entry.name[: -len("-toolchain-golden.tar.xz")]
parsed = _parse_sys_label(label)
entry_info: Dict[str, Any] = {
"golden_image": str(entry.resolve()),
"sys_label": parsed["full"],
"family": parsed.get("family", ""),
"target_id": parsed.get("target_id", ""),
"isa": parsed.get("isa", ""),
"opt": parsed.get("opt", ""),
"cross": parsed.get("cross", "0"),
}
# Merge manifest if available
manifest = _load_manifest(btc_root, parsed["full"])
if manifest:
entry_info["manifest"] = manifest
entry_info["clib"] = manifest.get("clib", "")
entry_info["triple"] = manifest.get("target_triple", "")
entry_info["description"] = manifest.get("description", "")
targets.append(entry_info)
return targets
# ---------------------------------------------------------------------------
# 6. merge_compiler_env -- main entry point for the executor
# ---------------------------------------------------------------------------
def merge_compiler_env(
project: Dict[str, Any],
node_config: Optional[Dict[str, Any]] = None,
btc_root: str = "/opt/BTC",
) -> Dict[str, str]:
"""Build a merged compiler environment combining ccache/distcc + BTC.
1. Calls :func:`build_ccache_env` for the standard environment.
2. Probes for BTC on this node.
3. If BTC is available, overlays BTC-specific CC/CXX/CFLAGS/CXXFLAGS/
LDFLAGS on top of the ccache env (BTC takes precedence).
4. Adds a ``toolchain_source`` key: ``"btc"``, ``"btc-fallback-gcc"``,
or ``"gcc"``.
If the node_config contains a ``btc.target`` key, it is used to
select a specific cross-compiler golden image when multiple are
installed.
Returns the fully merged environment dict.
"""
from backend.compiler_env import build_ccache_env
node_config = node_config or {}
env = build_ccache_env(project)
# Allow per-node target selection via node config
target = node_config.get("btc", {}).get("target") if isinstance(node_config.get("btc"), dict) else None
probe = probe_btc(btc_root, target_filter=target)
if not probe["available"]:
env["toolchain_source"] = "gcc"
return env
btc_env = btc_build_env(project, node_config, btc_root, target=target)
if btc_env.get("BTC_MODE") == "1":
# BTC takes precedence for compiler and flags
for key in ("CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS",
"BTC_MODE", "BTC_SYS_LABEL", "BTC_TARGET_ID",
"BTC_CROSS", "BTC_CLIB", "BTC_TARGET_TRIPLE"):
if key in btc_env:
env[key] = btc_env[key]
env["toolchain_source"] = "btc"
else:
env["toolchain_source"] = "btc-fallback-gcc"
return env