4"""Freestanding target runtime binary dependency and linker script ratchet.
6Enforces that target firmware images contain no implicit libc/newlib dependencies,
7no general-purpose heap, no unapproved dynamic allocation, and that linker
8scripts do not define heap anchors ('end', '_end') or '.heap' sections.
10Differentiates between:
11 - live symbols vs discarded symbols
12 - live archive members vs discarded sections vs LOAD-only archives
13 - project sbrk trap vs libnosys bump allocator
14 - allowed libgcc compiler helpers vs forbidden libc symbols
17from __future__
import annotations
28import freestanding_runtime_selftest
as fr_selftest
31_map_input_section_fields = 4
35def _repo_root() -> pathlib.Path:
36 """Return repository root path."""
37 return pathlib.Path(__file__).resolve().parents[2]
40def _forbidden_symbols() -> set[str]:
41 """Return the set of forbidden libc/allocator/stdio runtime symbols."""
61 "_malloc_usable_size_r",
76def _forbidden_archives() -> set[str]:
77 """Return the set of forbidden standard library archives."""
78 return {
"libnosys.a",
"libg_nano.a",
"libc_nano.a",
"libc.a"}
81def _allowed_compiler_archives() -> set[str]:
82 """Return compiler support archives that are explicitly allowed."""
83 return {
"libgcc.a",
"libm.a"}
86def _allowed_project_archives() -> set[str]:
87 """First-party build-product archives allowed to contribute live members."""
91 "libra8_shared_ek_ra8d2.a",
100def _allowed_libm_members() -> set[str]:
101 """Return explicitly approved libm.a members (transcendental functions without malloc/stdio)."""
108 "libm_a-sf_finite.o",
124 "libm_a-ef_rem_pio2.o",
125 "libm_a-math_errf.o",
126 "libm_a-sf_scalbn.o",
128 "libm_a-kf_rem_pio2.o",
132def _reviewed_runtime_primitives() -> set[str]:
133 """Explicit, reviewed allowlist for project-owned freestanding ABI primitives."""
153def find_tool(tool_name: str) -> str |
None:
154 """Locate tool binary in RA8_ARM_TOOLCHAIN_BIN, PATH, or standard toolchain paths."""
155 env_bin = os.environ.get(
"RA8_ARM_TOOLCHAIN_BIN")
157 p = pathlib.Path(env_bin) / tool_name
158 if p.is_file()
and os.access(p, os.X_OK):
161 path_dirs = os.environ.get(
"PATH",
"").split(os.pathsep)
164 p = pathlib.Path(d) / tool_name
165 if p.is_file()
and os.access(p, os.X_OK):
168 home_opt = str(pathlib.Path.home() /
"opt" /
"arm-gnu-toolchain-13.3" /
"bin")
171 "/opt/arm-gnu-toolchain/bin",
172 "/opt/toolchains/arm-gnu-toolchain-13.3/bin",
176 for d
in known_paths:
177 p = pathlib.Path(d) / tool_name
178 if p.is_file()
and os.access(p, os.X_OK):
184def parse_nm_symbols(nm_output: str) -> dict[str, str]:
185 """Parse nm output into {symbol_name: symbol_type}."""
186 symbols: dict[str, str] = {}
187 for line
in nm_output.splitlines():
189 if len(parts) > _nm_min_fields:
190 sym_type, sym_name = parts[1], parts[2]
191 symbols[sym_name] = sym_type
192 elif len(parts) == _nm_min_fields:
193 sym_type, sym_name = parts[0], parts[1]
194 symbols[sym_name] = sym_type
198def _parse_map_archive_member(
200 arch_member_re: re.Pattern[str],
201) -> tuple[str, str, bool] |
None:
202 """Extract archive name, member name, and whether it's a system archive."""
203 m = arch_member_re.search(obj_path)
206 arch_path, member = m.group(1), m.group(2)
207 arch_path_fwd = arch_path.replace(
"\\",
"/")
208 arch_name = arch_path_fwd.split(
"/")[-1]
210 is_system =
"/" in arch_path_fwd
and (
211 "arm-none-eabi" in arch_path_fwd
or "gcc" in arch_path_fwd
213 return arch_name, member, is_system
216def _parse_map_discarded(
218 arch_member_re: re.Pattern[str],
219) -> dict[str, set[str]]:
220 """Collect archive members that appear only under Discarded input sections."""
221 discarded: dict[str, set[str]] = {}
222 for line
in discarded_text.splitlines():
223 res = _parse_map_archive_member(line, arch_member_re)
225 arch_name, member, _ = res
226 discarded.setdefault(arch_name, set()).add(member)
230def _parse_map_memory_map(
231 memory_map_text: str,
232 arch_member_re: re.Pattern[str],
233) -> tuple[dict[str, set[str]], dict[str, str], set[str]]:
234 """Parse the memory map for live members, providers, and system archives."""
235 live_members: dict[str, set[str]] = {}
236 providers: dict[str, str] = {}
237 system_archives: set[str] = set()
238 lines = memory_map_text.splitlines()
240 for i, line
in enumerate(lines):
243 len(parts) >= _map_input_section_fields
244 and parts[0].startswith(
".")
245 and parts[1].startswith(
"0x")
246 and parts[2].startswith(
"0x")
249 size = int(parts[2], 16)
253 res = _parse_map_archive_member(parts[3], arch_member_re)
255 arch_name, member, is_system = res
256 live_members.setdefault(arch_name, set()).add(member)
258 system_archives.add(arch_name)
261 len(parts) == _map_symbol_fields
262 and parts[0].startswith(
"0x")
263 and not parts[1].startswith(
"0x")
268 res = _parse_map_archive_member(prev, arch_member_re)
270 providers[sym] = f
"{res[0]}({res[1]})"
272 obj_m = re.search(
r"([^\s()]+\.o(?:bj)?)", prev)
274 providers[sym] = obj_m.group(1).replace(
"\\",
"/").split(
"/")[-1]
276 return live_members, providers, system_archives
279def parse_map_file(map_content: str) -> dict[str, Any]:
280 """Parse GNU ld map file to extract live archive members and symbol providers."""
281 result: dict[str, Any] = {
282 "live_archive_members": {},
283 "discarded_archive_members": {},
284 "symbol_providers": {},
285 "system_archives": set(),
286 "has_heap_section":
False,
289 mm_pos = map_content.find(
"Linker script and memory map")
291 mm_pos = map_content.find(
"Memory Map")
293 memory_map_text = map_content[mm_pos:]
if mm_pos != -1
else ""
294 discarded_text = map_content[:mm_pos]
if mm_pos != -1
else map_content
296 heap_sec_re = re.compile(
r"^\s*\.heap\s+0x[0-9a-fA-F]+\s+0x[1-9a-fA-F]", re.MULTILINE)
297 if heap_sec_re.search(memory_map_text):
298 result[
"has_heap_section"] =
True
300 arch_member_re = re.compile(
r"([^\s()]+\.a)\(([^)]+\.o(?:bj)?)\)")
301 result[
"discarded_archive_members"] = _parse_map_discarded(discarded_text, arch_member_re)
303 live_members, providers, system_archives = _parse_map_memory_map(
304 memory_map_text, arch_member_re
306 result[
"live_archive_members"] = live_members
307 result[
"symbol_providers"] = providers
308 result[
"system_archives"] = system_archives
312def check_linker_script(content: str, path: str) -> list[str]:
313 """Check a linker script for prohibited 'end'/'_end' definitions and '.heap' sections."""
314 comment_re = re.compile(
r"/\*.*?\*/", re.DOTALL)
315 stripped = comment_re.sub(
" ", content)
318 end_sym_re = re.compile(
r"\b(PROVIDE\s*\(\s*)?(_?end)\s*=", re.MULTILINE)
320 f
"{path}: defines forbidden heap anchor '{m.group(2)}'"
321 for m
in end_sym_re.finditer(stripped)
324 heap_sec_re = re.compile(
r"(?<![a-zA-Z0-9_])\.heap\b")
325 if heap_sec_re.search(stripped):
326 violations.append(f
"{path}: defines forbidden '.heap' output section")
331def check_all_linker_scripts(repo_root: pathlib.Path, baseline: dict[str, Any]) -> list[str]:
332 """Check all target linker scripts in the repository."""
333 violations: list[str] = []
334 allowed_script_exceptions = set(baseline.get(
"linker_script_exceptions", []))
338 repo_root /
"examples",
343 for ld_path
in root.glob(
"**/*.ld"):
344 rel_path = str(ld_path.relative_to(repo_root)).replace(
"\\",
"/")
345 if "third_party" in rel_path:
348 content = ld_path.read_text(encoding=
"utf-8", errors=
"replace")
349 script_violations = check_linker_script(content, rel_path)
350 for v
in script_violations:
351 if rel_path
in allowed_script_exceptions:
358def check_source_asserts(content: str, path: str) -> list[str]:
359 """Check a source file for forbidden <assert.h> and runtime assert()."""
360 comment_re = re.compile(
r"/\*.*?\*/|//[^\n]*", re.DOTALL)
361 stripped = comment_re.sub(
" ", content)
362 str_re = re.compile(
r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'')
363 stripped = str_re.sub(
" ", stripped)
365 macro_def_re = re.compile(
r"#\s*define\s+assert\b[^\n]*")
366 stripped = macro_def_re.sub(
" ", stripped)
369 if re.search(
r"#\s*include\s+<assert\.h>", content):
370 violations.append(f
"{path}: includes forbidden libc <assert.h>")
372 assert_call_re = re.compile(
r"(?<![a-zA-Z0-9_])assert\s*\(")
373 for m
in assert_call_re.finditer(stripped):
374 line_num = content[: m.start()].count(
"\n") + 1
376 f
"{path}:{line_num}: uses forbidden standard assert() "
377 "(use RA8_ASSERT for runtime invariants or static_assert for compile-time)"
382def check_all_source_asserts(repo_root: pathlib.Path) -> list[str]:
383 """Check all target-linkable first-party source files for forbidden asserts."""
384 violations: list[str] = []
387 repo_root /
"examples",
393 exempt_fragments = (
"third_party",
"tests",
"apps/host",
"port/posix",
".pb-c.")
396 for p
in root.glob(
"**/*"):
397 if not p.is_file()
or p.suffix
not in (
".c",
".h"):
399 rel_path = str(p.relative_to(repo_root)).replace(
"\\",
"/")
400 if any(frag
in rel_path
for frag
in exempt_fragments):
402 content = p.read_text(encoding=
"utf-8", errors=
"replace")
403 violations.extend(check_source_asserts(content, rel_path))
408def _run_readelf_heap_check(readelf_bin: str |
None, elf_path: pathlib.Path) -> bool:
409 """Return True if readelf detects a .heap section."""
412 r_proc = subprocess.run(
413 [readelf_bin,
"-S", str(elf_path)],
418 if r_proc.returncode != 0:
420 return any(re.search(
r"\[\s*\d+\]\s+\.heap\b", line)
for line
in r_proc.stdout.splitlines())
423def _read_symbols_and_heap(
424 elf_path: pathlib.Path, nm_bin: str, readelf_bin: str |
None
425) -> tuple[dict[str, str], bool]:
426 """Run nm and readelf to extract defined symbols and check for .heap section."""
427 proc = subprocess.run(
428 [nm_bin,
"-n", str(elf_path)],
433 symbols = parse_nm_symbols(proc.stdout)
434 has_heap = _run_readelf_heap_check(readelf_bin, elf_path)
435 return symbols, has_heap
438def _load_map_info(map_path: pathlib.Path |
None, has_heap: bool) -> tuple[dict[str, Any], bool]:
439 """Parse map file if present, updating heap flag."""
440 map_info: dict[str, Any] = {
441 "live_archive_members": {},
442 "discarded_archive_members": {},
443 "symbol_providers": {},
444 "has_heap_section":
False,
446 if map_path
and map_path.is_file():
447 map_content = map_path.read_text(encoding=
"utf-8", errors=
"replace")
448 map_info = parse_map_file(map_content)
449 if map_info[
"has_heap_section"]:
451 return map_info, has_heap
455 elf_path: pathlib.Path,
456 map_path: pathlib.Path |
None =
None,
457 nm_binary: str |
None =
None,
458 readelf_binary: str |
None =
None,
460 """Analyze a single target ELF and its map file."""
462 candidate_map = elf_path.with_suffix(
".map")
463 if candidate_map.is_file():
464 map_path = candidate_map
466 nm_bin = nm_binary
or find_tool(
"arm-none-eabi-nm")
467 readelf_bin = readelf_binary
or find_tool(
"arm-none-eabi-readelf")
469 msg =
"arm-none-eabi-nm tool not found"
470 raise RuntimeError(msg)
472 symbols, has_heap_section = _read_symbols_and_heap(elf_path, nm_bin, readelf_bin)
473 map_info, has_heap_section = _load_map_info(map_path, has_heap_section)
474 all_forbidden = _forbidden_symbols()
475 live_forbidden_syms = sorted(s
for s
in symbols
if s
in all_forbidden)
477 sbrk_present =
"_sbrk" in symbols
478 sbrk_provider = map_info[
"symbol_providers"].get(
"_sbrk",
"unknown" if sbrk_present
else "none")
479 end_sym_present = (
"end" in symbols)
or (
"_end" in symbols)
480 reviewed_prims = _reviewed_runtime_primitives()
482 s: map_info[
"symbol_providers"].get(s,
"project")
for s
in symbols
if s
in reviewed_prims
484 posix_prefixes = (
"ra8_io_stream_posix",
"fw_fs_posix",
"fw_if_fs_posix")
485 posix_symbols = sorted(s
for s
in symbols
if any(s.startswith(p)
for p
in posix_prefixes))
487 s: prov
for s, prov
in map_info[
"symbol_providers"].items()
if "posix" in prov.lower()
491 "elf": str(elf_path),
492 "map": str(map_path)
if map_path
else None,
493 "live_forbidden_symbols": live_forbidden_syms,
494 "live_archive_members": {k: sorted(v)
for k, v
in map_info[
"live_archive_members"].items()},
495 "system_archives": sorted(map_info.get(
"system_archives", set())),
496 "discarded_archive_members": {
497 k: sorted(v)
for k, v
in map_info[
"discarded_archive_members"].items()
499 "runtime_primitive_providers": prim_providers,
500 "sbrk_present": sbrk_present,
501 "sbrk_provider": sbrk_provider,
502 "end_symbol_present": end_sym_present,
503 "has_heap_section": has_heap_section,
504 "posix_symbols": posix_symbols,
505 "posix_providers": posix_providers,
509def _check_live_archive_policy(app_name: str, analysis: dict[str, Any]) -> list[str]:
510 """Fail closed on live archives outside the explicit allowlists."""
511 violations: list[str] = []
512 allowed = _allowed_compiler_archives()
513 project = _allowed_project_archives()
514 live = analysis.get(
"live_archive_members", {})
515 for arch
in sorted(live):
518 unapproved = [m
for m
in live[arch]
if m
not in _allowed_libm_members()]
521 f
"{app_name}: contains unapproved libm.a members: {sorted(unapproved)}"
523 elif arch
in project
or arch
in _forbidden_archives():
526 violations.append(f
"{app_name}: contains unapproved live archive '{arch}'")
530def _split_archive_provider(provider: str) -> tuple[str, str] |
None:
531 """Split an 'archive(member)' provider into its (archive, member) pair."""
532 match = re.match(
r"^(.+\.a)\(([^)]+)\)$", provider)
535 arch = match.group(1).replace(
"\\",
"/").split(
"/")[-1]
536 return arch, match.group(2)
539def _check_primitive_providers(app_name: str, analysis: dict[str, Any]) -> list[str]:
540 """Reject runtime primitives resolved to unapproved archives."""
541 violations: list[str] = []
542 allowed = _allowed_compiler_archives() | _allowed_project_archives()
543 for sym, provider
in analysis.get(
"runtime_primitive_providers", {}).items():
544 if any(arch
in provider
for arch
in _forbidden_archives()):
546 f
"{app_name}: runtime primitive '{sym}' resolved to forbidden '{provider}'"
549 split = _split_archive_provider(provider)
553 if arch
not in allowed
or (arch ==
"libm.a" and member
not in _allowed_libm_members()):
555 f
"{app_name}: runtime primitive '{sym}' resolved to unapproved '{provider}'"
560def _check_posix_block(app_name: str, analysis: dict[str, Any]) -> list[str]:
561 """Reject host-only POSIX symbols and objects in target images."""
562 violations: list[str] = []
563 if analysis.get(
"posix_symbols"):
565 f
"{app_name}: contains forbidden host-only POSIX symbol(s): {analysis['posix_symbols']}"
567 if analysis.get(
"posix_providers"):
568 providers = analysis[
"posix_providers"]
569 violations.append(f
"{app_name}: links host-only POSIX object(s): {providers}")
573def _check_sbrk_ratchet(
574 app_name: str, analysis: dict[str, Any], app_debt: dict[str, Any]
576 """Reject unexpected live _sbrk against the recorded provider."""
577 violations: list[str] = []
578 expected_provider = app_debt.get(
"sbrk_provider",
"none")
579 if expected_provider ==
"none" and analysis[
"sbrk_present"]:
581 f
"{app_name}: unexpected live _sbrk symbol (provider: '{analysis['sbrk_provider']}')"
584 analysis[
"sbrk_present"]
585 and expected_provider !=
"none"
586 and expected_provider != analysis[
"sbrk_provider"]
589 f
"{app_name}: _sbrk provider changed from '{expected_provider}' "
590 f
"to '{analysis['sbrk_provider']}'"
595def _eval_strict_freestanding(
597 analysis: dict[str, Any],
599 """Enforce strict freestanding invariants for images with zero allowable debt."""
600 violations: list[str] = []
601 live_syms = set(analysis[
"live_forbidden_symbols"])
603 violations.append(f
"{app_name}: contains forbidden runtime symbols: {sorted(live_syms)}")
605 violations.extend(_check_live_archive_policy(app_name, analysis))
607 live_archives = analysis[
"live_archive_members"]
608 for arch
in _forbidden_archives():
609 members = live_archives.get(arch, [])
611 violations.append(f
"{app_name}: contains forbidden live members from {arch}: {members}")
613 violations.extend(_check_primitive_providers(app_name, analysis))
615 if analysis[
"sbrk_present"]:
617 f
"{app_name}: unexpected live _sbrk symbol (provider: '{analysis['sbrk_provider']}')"
620 if analysis[
"end_symbol_present"]:
621 violations.append(f
"{app_name}: defines heap anchor symbol 'end'/'_end'")
623 violations.extend(_check_posix_block(app_name, analysis))
627def _eval_ratchet_baseline(
629 analysis: dict[str, Any],
630 app_debt: dict[str, Any],
632 """Enforce that an image does not exceed its recorded debt."""
633 violations: list[str] = []
634 live_syms = set(analysis[
"live_forbidden_symbols"])
635 baseline_syms = set(app_debt.get(
"forbidden_symbols", []))
636 new_syms = live_syms - baseline_syms
639 f
"{app_name}: introduces new forbidden symbols beyond baseline: {sorted(new_syms)}"
644 violations.extend(_check_live_archive_policy(app_name, analysis))
646 live_archives = analysis[
"live_archive_members"]
647 baseline_archives = app_debt.get(
"forbidden_archives", {})
648 for arch
in _forbidden_archives():
649 current_members = set(live_archives.get(arch, []))
650 base_members = set(baseline_archives.get(arch, []))
651 new_members = current_members - base_members
654 f
"{app_name}: introduces new live members from {arch} "
655 f
"beyond baseline: {sorted(new_members)}"
660 violations.extend(_check_primitive_providers(app_name, analysis))
662 violations.extend(_check_sbrk_ratchet(app_name, analysis, app_debt))
664 expected_end = app_debt.get(
"end_symbol",
False)
665 if analysis[
"end_symbol_present"]
and not expected_end:
666 violations.append(f
"{app_name}: defines unexpected heap anchor 'end'/'_end'")
668 violations.extend(_check_posix_block(app_name, analysis))
673def _is_zero_debt(app_debt: dict[str, Any]) -> bool:
674 """Return True if an app baseline entry specifies zero allowable debt."""
675 forbidden_syms = app_debt.get(
"forbidden_symbols", [])
676 forbidden_archs = app_debt.get(
"forbidden_archives", {})
677 has_arch_members = any(bool(members)
for members
in forbidden_archs.values())
678 sbrk_provider = app_debt.get(
"sbrk_provider",
"none")
679 end_symbol = app_debt.get(
"end_symbol",
False)
682 and not has_arch_members
683 and sbrk_provider
in (
None,
"none")
688def evaluate_against_baseline(
690 analysis: dict[str, Any],
691 baseline: dict[str, Any],
693 """Check whether the analyzed image violates the freestanding ratchet."""
694 violations: list[str] = []
695 if analysis.get(
"has_heap_section"):
696 violations.append(f
"{app_name}: contains forbidden '.heap' section")
698 app_debt = baseline.get(
"apps", {}).get(app_name)
701 violations.extend(_eval_strict_freestanding(app_name, analysis))
702 elif _is_zero_debt(app_debt):
704 violations.extend(_eval_strict_freestanding(app_name, analysis))
706 violations.extend(_eval_ratchet_baseline(app_name, analysis, app_debt))
711def _check_ra8_freestanding_build_files(repo_root: pathlib.Path) -> list[str]:
712 """Verify build-system files discriminate target builds with RA8_FREESTANDING."""
713 failures: list[str] = []
717 toolchain = repo_root /
"cmake" /
"toolchain-ra8d2.cmake"
718 if not toolchain.is_file():
719 failures.append(
"cmake/toolchain-ra8d2.cmake not found")
720 elif "RA8_FREESTANDING" not in toolchain.read_text(encoding=
"utf-8"):
721 failures.append(
"cmake/toolchain-ra8d2.cmake does not define RA8_FREESTANDING")
723 add_app = repo_root /
"cmake" /
"ra8_add_app.cmake"
724 if not add_app.is_file():
725 failures.append(
"cmake/ra8_add_app.cmake not found")
726 elif "RA8_FREESTANDING" not in add_app.read_text(encoding=
"utf-8"):
727 failures.append(
"cmake/ra8_add_app.cmake does not define RA8_FREESTANDING")
732 entries: list[dict[str, Any]],
738 """Check one compile database for the RA8_FREESTANDING discriminator."""
739 failures: list[str] = []
740 for entry
in entries:
741 cmd = entry.get(
"command",
" ".join(entry.get(
"arguments", [])))
742 if exempt
is not None and exempt
in cmd:
744 has_flag =
"-DRA8_FREESTANDING" in cmd
745 if must_have
and not has_flag:
746 rel_file = entry.get(
"file",
"unknown")
747 failures.append(f
"{label} missing -DRA8_FREESTANDING: {rel_file}")
749 if not must_have
and has_flag:
750 rel_file = entry.get(
"file",
"unknown")
751 failures.append(f
"{label} sets -DRA8_FREESTANDING: {rel_file}")
756def _load_db_entries(db_path: pathlib.Path) -> list[dict[str, Any]] |
None:
757 """Load compile database entries, or None when unreadable."""
759 entries = json.loads(db_path.read_text(encoding=
"utf-8"))
760 except (OSError, json.JSONDecodeError):
762 return entries
if isinstance(entries, list)
else None
765def discover_target_dbs(repo_root: pathlib.Path) -> list[pathlib.Path]:
766 """Find per-app target compile databases under gitignored build dirs."""
767 dbs: list[pathlib.Path] = []
768 for top
in (
"examples",
"apps"):
769 base = repo_root / top
772 p
for p
in sorted(base.glob(
"**/build/compile_commands.json"))
if p.is_file()
777def check_target_db_flags(db_path: pathlib.Path) -> list[str]:
778 """Require every TU in one target database to define RA8_FREESTANDING."""
779 entries = _load_db_entries(db_path)
if db_path.is_file()
else None
781 return [f
"Target compile database unreadable or missing: {db_path}"]
783 return [f
"Target compile database empty: {db_path}"]
784 return check_db_entries(entries, must_have=
True, exempt=
None, label=
"Target compile command")
787def check_discovered_target_dbs(repo_root: pathlib.Path) -> list[str]:
788 """Enforce the discriminator on every generated per-app target database."""
789 failures: list[str] = []
790 for db_path
in discover_target_dbs(repo_root):
791 failures.extend(check_target_db_flags(db_path))
795def _check_ra8_freestanding_config(repo_root: pathlib.Path) -> list[str]:
796 """Verify RA8_FREESTANDING is defined for target builds and absent for host builds."""
797 failures = _check_ra8_freestanding_build_files(repo_root)
799 target_db_path = repo_root /
"compile_commands.json"
800 if target_db_path.is_file():
801 entries = _load_db_entries(target_db_path)
802 if entries
is not None:
805 entries, must_have=
True, exempt=
None, label=
"Target compile command"
809 failures.extend(check_discovered_target_dbs(repo_root))
813 repo_root /
"tests" /
"build-darwin" /
"compile_commands.json",
814 repo_root /
"tests" /
"build-linux" /
"compile_commands.json",
823 entries = _load_db_entries(host_db_path)
824 if entries
is not None:
829 exempt=
"-DRA8_TEST_FREESTANDING",
830 label=
"Host compile command",
837def selftest() -> int:
838 """Run all freestanding-runtime selftests."""
839 failures = fr_selftest.run_selftests(_repo_root())
840 failures.extend(_check_ra8_freestanding_config(_repo_root()))
842 print(f
"check_freestanding_runtime selftest: {len(failures)} failure(s):", file=sys.stderr)
844 print(f
" FAIL: {f}", file=sys.stderr)
846 print(
"check_freestanding_runtime selftest: all cases pass (both directions).")
850def _load_baseline(baseline_path: pathlib.Path) -> dict[str, Any] |
None:
851 """Load baseline dictionary from JSON file."""
852 if not baseline_path.is_file():
855 return json.loads(baseline_path.read_text(encoding=
"utf-8"))
856 except (OSError, json.JSONDecodeError)
as e:
857 print(f
"Error reading baseline file {baseline_path}: {e}", file=sys.stderr)
861def _process_elf_list(
862 elfs: list[tuple[str, pathlib.Path, pathlib.Path |
None]],
863 baseline: dict[str, Any],
864 violations: list[str],
868 """Analyze ELFs and evaluate against baseline or update baseline."""
869 for app_name, elf_path, map_file
in elfs:
871 analysis = analyze_image(elf_path, map_path=map_file)
872 except (OSError, subprocess.SubprocessError, RuntimeError)
as e:
873 print(f
"Error analyzing {elf_path}: {e}", file=sys.stderr)
877 baseline.setdefault(
"apps", {})[app_name] = {
878 "forbidden_symbols": analysis[
"live_forbidden_symbols"],
879 "forbidden_archives": {
881 for k, v
in analysis[
"live_archive_members"].items()
882 if k
in _forbidden_archives()
884 "sbrk_provider": analysis[
"sbrk_provider"],
885 "end_symbol": analysis[
"end_symbol_present"],
888 violations.extend(evaluate_against_baseline(app_name, analysis, baseline))
893 scan_dir: pathlib.Path |
None,
894 elf: pathlib.Path |
None,
895 map_path: pathlib.Path |
None,
896 app_name: str |
None,
897) -> list[tuple[str, pathlib.Path, pathlib.Path |
None]]:
898 """Gather ELF targets to inspect."""
899 elfs: list[tuple[str, pathlib.Path, pathlib.Path |
None]] = []
901 elfs.extend((app_name
or p.stem, p,
None)
for p
in scan_dir.glob(
"**/*.elf"))
903 elfs.append((app_name
or elf.stem, elf, map_path))
907def _run_static_checks(
908 args: argparse.Namespace, root: pathlib.Path, baseline: dict[str, Any]
910 """Run static source-assert and linker-script checks."""
911 if args.check_asserts:
912 assert_violations = check_all_source_asserts(root)
913 if assert_violations:
915 f
"FATAL: Source assert violations ({len(assert_violations)}):",
918 for v
in assert_violations:
919 print(f
" {v}", file=sys.stderr)
921 print(
"check_freestanding_runtime: target source clean (no standard libc assert).")
923 if args.check_scripts:
924 script_violations = check_all_linker_scripts(root, baseline)
925 if script_violations:
927 f
"FATAL: Linker script violations ({len(script_violations)}):",
930 for v
in script_violations:
931 print(f
" {v}", file=sys.stderr)
934 "check_freestanding_runtime: linker scripts clean (no unauthorized 'end' or '.heap')."
940 """CLI entry point."""
941 parser = argparse.ArgumentParser(description=
"Freestanding runtime dependency ratchet.")
942 parser.add_argument(
"--selftest", action=
"store_true", help=
"Run self-tests.")
943 parser.add_argument(
"--check-scripts", action=
"store_true", help=
"Audit target linker scripts.")
944 parser.add_argument(
"--check-asserts", action=
"store_true", help=
"Audit for libc assert.")
945 parser.add_argument(
"--elf", type=pathlib.Path, help=
"Target ELF to inspect.")
946 parser.add_argument(
"--map", type=pathlib.Path, help=
"Linker map file to inspect.")
947 parser.add_argument(
"--app-name", type=str, help=
"Application name for baseline check.")
948 parser.add_argument(
"--scan-dir", type=pathlib.Path, help=
"Scan a directory for .elf files.")
949 parser.add_argument(
"--baseline", type=pathlib.Path, help=
"Path to baseline JSON file.")
950 parser.add_argument(
"--update-baseline", action=
"store_true", help=
"Update baseline file.")
952 args = parser.parse_args()
957 baseline_path = args.baseline
or (root /
".github" /
"freestanding-runtime-baseline.json")
958 baseline = _load_baseline(baseline_path)
961 baseline.setdefault(
"linker_script_exceptions", [])
963 violations: list[str] = []
965 static_rc = _run_static_checks(args, root, baseline)
966 elfs_to_check = _collect_elfs(args.scan_dir, args.elf, args.map, args.app_name)
968 rc = static_rc
or _process_elf_list(
969 elfs_to_check, baseline, violations, update=args.update_baseline
974 db_violations = check_target_db_flags(args.scan_dir /
"compile_commands.json")
977 f
"FATAL: Freestanding target misconfigured ({len(db_violations)}):",
980 for v
in db_violations:
981 print(f
" {v}", file=sys.stderr)
984 if args.update_baseline:
985 baseline_path.write_text(
986 json.dumps(baseline, indent=2, sort_keys=
True) +
"\n",
989 print(f
"check_freestanding_runtime: updated baseline at {baseline_path}")
991 print(f
"FATAL: Freestanding runtime violations ({len(violations)}):", file=sys.stderr)
993 print(f
" {v}", file=sys.stderr)
999if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.