blob: 107adb387ba20f6e665a21e8cb19da2bda3e6e64 [file] [edit]
#!/usr/bin/env python3
"""
Append an include-tree comment after the contents of each `memmap*.ld` under
`src/*/pico_platform/`.
The comment matches the output of `memmap_include_tree.py` (two columns, no color).
Re-running replaces a previous block marked `BEGIN_MEMMAP_INCLUDE_TREE` /
`END_MEMMAP_INCLUDE_TREE` (at the end of the file, or at the beginning for older runs).
A companion `memmap_xxx.cmake` file is written next to each `memmap_xxx.ld`,
containing `pico_add_link_depend(pico_standard_link ...)` for every transitively
included linker-script fragment of that memmap, with paths expressed as
`${PICO_SDK_PATH}/...` relative to the SDK root.
Examples:
./tools/memmap_annotate.py # annotates all memmap*.ld files in the SDK
./tools/memmap_annotate.py --ci # does not write files; exits with failure if any memmap would need updating
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
_TOOLS = Path(__file__).resolve().parent
if str(_TOOLS) not in sys.path:
sys.path.insert(0, str(_TOOLS))
import memmap_include_tree as inc_tree
_ANNOT_BEGIN = "/* BEGIN_MEMMAP_INCLUDE_TREE\n"
_ANNOT_END = " * END_MEMMAP_INCLUDE_TREE */\n"
_ANNOT_BLOCK_CORE = (
r"/\* BEGIN_MEMMAP_INCLUDE_TREE\n.*?\n \* END_MEMMAP_INCLUDE_TREE \*/\s*"
)
def strip_annotation_block(text: str) -> str:
"""Remove a previously inserted annotation block (trailing, or legacy leading)."""
t = text
# Preferred: block at end of file
m = re.search(_ANNOT_BLOCK_CORE + r"\Z", t, re.DOTALL)
if m:
head = t[: m.start()].rstrip("\n")
if head:
head += "\n"
return head
# Legacy: block was prepended at the beginning
m2 = re.match(_ANNOT_BLOCK_CORE + r"\n?", t, re.DOTALL)
if m2:
return t[m2.end() :].lstrip("\n")
return t
def build_annotation_block(tree_lines: list[str]) -> str:
parts = [
_ANNOT_BEGIN,
" * This is the default include tree for this file (generated by tools/memmap_annotate.py\n"
" * and tools/memmap_include_tree.py). To override any files in this tree, see the\n"
" * pico_add_linker_script_override_path CMake function under pico_standard_link.\n",
" *\n",
]
for ln in tree_lines:
parts.append(" * " + ln + "\n")
parts.append(_ANNOT_END)
return "".join(parts)
def collect_included_files(nodes: list[inc_tree.ChildRow], acc: set[Path] | None = None) -> set[Path]:
"""Recursively collect all resolved file paths from an include tree (excludes the root)."""
if acc is None:
acc = set()
for path, subs, _ in nodes:
if path.exists():
acc.add(path)
collect_included_files(subs, acc)
return acc
def build_cmake_block(included_files: set[Path], sdk_root: Path, memmap_stem: str) -> str:
func_name = "pico_add_memmap_link_depends_internal"
lines = [
"# Auto-generated by tools/memmap_annotate.py — do not edit by hand\n",
f"function({func_name} TARGET)\n",
]
for p in sorted(included_files):
try:
rel = p.relative_to(sdk_root)
except ValueError:
rel = p
lines.append(
f" pico_add_link_depend(${{TARGET}} ${{PICO_SDK_PATH}}/{rel.as_posix()})\n"
)
lines.append("endfunction()\n")
return "".join(lines)
def annotate_one(
path: Path,
sdk_root: Path,
platform: str | None,
*,
ci: bool,
) -> tuple[bool, str]:
"""Return (ok, status message). ok is False on unresolved includes."""
path = path.resolve()
search_dirs = inc_tree.default_search_dirs(sdk_root, path, platform)
visited: set[Path] = set()
user_l_set: frozenset[Path] = frozenset()
user_l_labels: dict[Path, str] = {}
root_node, root_children = inc_tree.walk_includes(
path, search_dirs, user_l_set, user_l_labels, visited
)
missing = inc_tree.collect_unresolved_include_paths(root_children)
if missing:
names = ", ".join(m.name for m in missing)
return False, f"unresolved INCLUDE: {names}"
included_files = collect_included_files(root_children)
lines = inc_tree.format_tree_lines(root_node, root_children, color=False)
block = build_annotation_block(lines)
old_ld = path.read_text(encoding="utf-8", errors="replace")
body = strip_annotation_block(old_ld)
if body and not body.endswith("\n"):
body += "\n"
new_ld = body + "\n" + block if body else block
cmake_path = path.with_suffix(".cmake")
new_cmake = build_cmake_block(included_files, sdk_root, path.stem)
old_cmake = cmake_path.read_text(encoding="utf-8", errors="replace") if cmake_path.exists() else ""
if new_ld == old_ld and new_cmake == old_cmake:
return True, "unchanged"
if ci:
return True, "needs updating"
if new_ld != old_ld:
path.write_text(new_ld, encoding="utf-8")
if new_cmake != old_cmake:
cmake_path.write_text(new_cmake, encoding="utf-8")
return True, "updated"
def parse_args(argv: list[str]) -> argparse.Namespace:
p = argparse.ArgumentParser(
description=inc_tree.fold_docstring_for_argparse(__doc__ or ""),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument(
"--sdk-root",
type=Path,
default=None,
help=inc_tree.CLI_HELP_SDK_ROOT,
)
p.add_argument(
"--platform",
default=None,
metavar="CHIP",
help=inc_tree.CLI_HELP_PLATFORM,
)
p.add_argument(
"--ci",
action="store_true",
help="Do not write files; exit with failure if any memmap would need updating",
)
p.add_argument(
"paths",
nargs="*",
type=Path,
metavar="MEMMAP.LD",
help="Linker scripts to annotate (default: all src/*/pico_platform/memmap*.ld)",
)
return p.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
sdk_root = args.sdk_root
if args.paths:
files = [p.resolve() for p in args.paths]
for f in files:
if not f.is_file():
print(f"error: not a file: {f}", file=sys.stderr)
return 1
if sdk_root is None:
sdk_root = inc_tree.infer_sdk_root(files[0])
if sdk_root is None:
print(inc_tree.CLI_MSG_SDK_ROOT_INFER_FAILED, file=sys.stderr)
return 1
else:
if sdk_root is None:
sdk_root = inc_tree.infer_sdk_root(Path.cwd())
if sdk_root is None:
print(inc_tree.CLI_MSG_SDK_ROOT_INFER_FAILED, file=sys.stderr)
return 1
files = sorted(sdk_root.glob("src/*/pico_platform/memmap*.ld"))
if not files:
print("error: no memmap*.ld files to process", file=sys.stderr)
return 1
sdk_root = sdk_root.resolve()
if args.platform is not None:
perr = inc_tree.validate_explicit_platform(sdk_root, args.platform)
if perr:
print(f"error: {perr}", file=sys.stderr)
return 1
err = 0
for f in files:
plat = args.platform or inc_tree.infer_platform_chip(f)
ok, msg = annotate_one(f, sdk_root, plat, ci=args.ci)
try:
tag = str(f.relative_to(sdk_root))
except ValueError:
tag = str(f)
if ok:
print(inc_tree.format_memmap_annotate_status_line(tag, msg))
if args.ci and msg == "needs updating":
err = 1
else:
print(f"{tag}: {msg}", file=sys.stderr)
err = 1
return err
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))