805 lines
29 KiB
Python
Executable File
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 |