#!/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)