mirror of
https://github.com/meshtastic/firmware.git
synced 2026-08-01 10:58:30 -04:00
141 lines
5.7 KiB
Python
141 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
# trunk-ignore-all(ruff/F821)
|
|
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
|
#
|
|
# post:extra_scripts/nrf54l15_linker.py
|
|
#
|
|
# Fix for Zephyr two-pass link on nRF54L15:
|
|
# platformio-build.py registers env.Depends("$PROG_PATH", final_ld_script) but
|
|
# the SCons dependency chain is broken (final_ld_script Command never runs).
|
|
# This script adds a PreAction on the final firmware binary that runs the gcc
|
|
# preprocessing command directly (extracted from build.ninja) to generate
|
|
# zephyr/linker.cmd before the link step.
|
|
#
|
|
# PlatformIO bundles an old Ninja that can't handle multi-output depslog rules,
|
|
# so we parse the COMMAND line from build.ninja and run just the gcc -E part,
|
|
# skipping the cmake_transform_depfile step (only needed for Ninja deps tracking).
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
Import("env")
|
|
|
|
if env.get("PIOENV") != "nrf54l15dk":
|
|
pass # Only for the nrf54l15dk environment
|
|
else:
|
|
|
|
def _extract_gcc_command(ninja_build):
|
|
"""Parse build.ninja to find the gcc -E command that generates linker.cmd.
|
|
|
|
The rule format depends on the host:
|
|
Windows (CMake's RunCMake wraps every command):
|
|
COMMAND = cmd.exe /C "cd /D DIR && arm-none-eabi-gcc.exe ... -o linker.cmd && cmake.exe -E cmake_transform_depfile ..."
|
|
POSIX (Linux/macOS - no wrapper):
|
|
COMMAND = cd DIR && arm-none-eabi-gcc ... -o linker.cmd && cmake -E cmake_transform_depfile ...
|
|
|
|
Returns (gcc_cmd_string, cwd_path) or raises RuntimeError.
|
|
"""
|
|
in_rule = False
|
|
with open(ninja_build, "r", encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
# Detect start of the linker.cmd custom command rule
|
|
if not in_rule:
|
|
if "build zephyr/linker.cmd" in line and "CUSTOM_COMMAND" in line:
|
|
in_rule = True
|
|
continue
|
|
|
|
stripped = line.strip()
|
|
if not stripped.startswith("COMMAND = "):
|
|
continue
|
|
|
|
command_val = stripped[len("COMMAND = ") :]
|
|
|
|
# On Windows the value is wrapped in `cmd.exe /C "..."` - strip
|
|
# the wrapper. On POSIX hosts the inner sequence is the value
|
|
# itself (no quoting layer).
|
|
m = re.search(r'/C\s+"(.*)"\s*$', command_val)
|
|
inner = m.group(1) if m else command_val
|
|
parts = inner.split(" && ")
|
|
|
|
cwd = None
|
|
gcc_cmd = None
|
|
for part in parts:
|
|
part = part.strip()
|
|
if part.startswith("cd /D "): # Windows form
|
|
cwd = part[len("cd /D ") :]
|
|
elif part.startswith("cd "): # POSIX form
|
|
cwd = part[len("cd ") :]
|
|
elif "arm-none-eabi-gcc" in part:
|
|
gcc_cmd = part
|
|
|
|
if not gcc_cmd:
|
|
raise RuntimeError(
|
|
"nRF54L15 linker fix: arm-none-eabi-gcc command not found in:\n%s"
|
|
% inner[:400]
|
|
)
|
|
|
|
return gcc_cmd, cwd
|
|
|
|
raise RuntimeError(
|
|
"nRF54L15 linker fix: 'build zephyr/linker.cmd' rule not found in build.ninja"
|
|
)
|
|
|
|
def _generate_linker_cmd(target, source, env):
|
|
"""Generate zephyr/linker.cmd via direct gcc invocation before the final link."""
|
|
build_dir = env.subst("$BUILD_DIR")
|
|
zephyr_dir = os.path.join(build_dir, "zephyr")
|
|
linker_cmd = os.path.join(zephyr_dir, "linker.cmd")
|
|
|
|
if os.path.exists(linker_cmd):
|
|
return # Already present - nothing to do
|
|
|
|
ninja_build = os.path.join(build_dir, "build.ninja")
|
|
if not os.path.exists(ninja_build):
|
|
raise RuntimeError(
|
|
"nRF54L15 linker fix: build.ninja not found at %s\n"
|
|
"Run a full build first so CMake generates the Ninja files."
|
|
% ninja_build
|
|
)
|
|
|
|
gcc_cmd, cwd = _extract_gcc_command(ninja_build)
|
|
run_cwd = cwd if cwd else zephyr_dir
|
|
|
|
print(
|
|
"==> nRF54L15: Generating zephyr/linker.cmd (LINKER_ZEPHYR_FINAL) via GCC"
|
|
)
|
|
# gcc_cmd comes verbatim from our own build.ninja (never user input) and
|
|
# contains Windows-style paths with spaces that cannot be safely argv-split
|
|
# with shlex, so we run it via the platform shell. nosec/nosemgrep below
|
|
# acknowledge this deliberate, scoped use of shell=True.
|
|
result = subprocess.run( # nosec B602
|
|
gcc_cmd,
|
|
shell=True, # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
|
|
cwd=run_cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
print("GCC stdout:", result.stdout[:2000])
|
|
print("GCC stderr:", result.stderr[:2000])
|
|
raise RuntimeError(
|
|
"nRF54L15 linker fix: GCC failed to generate linker.cmd (rc=%d)"
|
|
% result.returncode
|
|
)
|
|
if not os.path.exists(linker_cmd):
|
|
raise RuntimeError(
|
|
"nRF54L15 linker fix: GCC returned 0 but linker.cmd was not created at %s"
|
|
% linker_cmd
|
|
)
|
|
print("==> linker.cmd generated successfully")
|
|
|
|
# Use PIOMAINPROG (set by ZephyrBuildProgram) to get the exact SCons node
|
|
prog = env.get("PIOMAINPROG")
|
|
if prog:
|
|
env.AddPreAction(prog, _generate_linker_cmd)
|
|
else:
|
|
print(
|
|
"[nrf54l15_linker] WARNING: PIOMAINPROG not set, falling back to $PROG_PATH"
|
|
)
|
|
env.AddPreAction(env.subst("$PROG_PATH"), _generate_linker_cmd)
|