Skip to content

Feature Relinker: move functions from IRAM to Flash#448

Merged
Jason2866 merged 110 commits into
developfrom
relinker
Mar 31, 2026
Merged

Feature Relinker: move functions from IRAM to Flash#448
Jason2866 merged 110 commits into
developfrom
relinker

Conversation

@Jason2866
Copy link
Copy Markdown

@Jason2866 Jason2866 commented Mar 29, 2026

Summary by CodeRabbit

  • New Features

    • CSV-driven Relinker (CLI + API) to remap functions and integrate relinked linker scripts into builds.
    • Pre-build generation pass to produce missing generated sources before compilation.
    • Improved handling of generated-source object paths under build directories.
  • Bug Fixes

    • Validates relinker option combinations with clearer error/warning behavior.
    • ELF build now depends on relinked linker script when relinker is enabled.
    • Venv dependency hashing and recreation when interpreter or deps change.
  • Documentation

    • Detailed Relinker integration guide and quick-start.
  • Tests

    • New unit, integration, functionality suites, runner, and test docs.

Checklist:

  • The pull request is done against the latest develop branch
  • Only relevant files were touched
  • Only one feature/fix was added per PR, more changes are allowed when changing boards.json
  • I accept the CLA

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50317390-52f5-4887-9984-aaea58ee611d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new CSV/sdkconfig-driven Relinker package and integrates it into the ESP-IDF PlatformIO build: prebuilds Ninja-generated sources, post-processes ldgen-generated sections.ld into sections.ld.relinked (optional via platformio.ini settings), adjusts object path derivation for generated sources, and refactors Python dependency/venv handling with dependency hashing and venv recreation.

Changes

Cohort / File(s) Summary
Relinker core
builder/relinker/__init__.py, builder/relinker/configuration.py, builder/relinker/relinker.py
New relinker package: CSV/sdkconfig parsing, IDF_PATH-aware path resolution, objdump-driven function→section discovery, descriptor filtering/merging, CLI + API (main, run_relinker) that rewrite ldgen output to produce sections.ld.relinked. Review objdump invocation, LC_ALL usage, wildcard CSV handling, path normalization, missing-function warning vs error logic, and ldgen EntityDB dynamic import.
ESP-IDF SCons integration
builder/frameworks/espidf.py
Adds _ensure_generated_sources() to parse build.ninja and run ninja -C <build_dir> for missing CUSTOM_COMMAND outputs; compile_source_files() invokes this pre-pass and improves object-path derivation for generated sources; generate_project_ld_script() now returns the LD node and optionally validates/normalizes custom_relinker_{library,object,function} options, creates a relinker stamp sections.ld.relinked, and wires env.Depends(<elf>, sections.ld.relinked) when configured. Refactors Python deps handling into _get_python_deps() and install_python_deps(deps=None), adds deps-hash (pio-idf-venv.json) and venv recreation when outdated.
Relinker docs / Guide
RELINKER_INTEGRATION.md
New integration guide detailing CSV formats, custom_relinker_* settings, custom_relinker_missing_function_info semantics, quick-start examples, relocation constraints, troubleshooting, and a full ESP32-C2 configuration example.
Tests / QA
test/...
test/README.md, test/TESTING.md, test/VALIDATION_REPORT.md, test/run_tests.py, test/test_configuration.py, test/test_integration.py, test/test_relinker.py, test/test_relinker_functionality.py, test/.gitignore
Comprehensive tests and harness: unit, integration, and functionality tests for sdkconfig parsing, path normalization ($IDF_PATH), objdump mapping, linker-script edits, idempotency, and end-to-end flows. Adds test runner and docs. Review for brittle external assumptions (env vars, objdump/ldgen availability) and mocked subprocess usage.
Misc / Stubs
builder/relinker/__init__.py, test/.gitignore
Package init placeholder and test .gitignore added.

Sequence Diagram

sequenceDiagram
    participant Env as Build Env
    participant Ninja as Ninja
    participant SCons as SCons
    participant LDGen as ldgen (sections.ld)
    participant Relinker as Relinker (Python)
    participant Linker as Final Linker

    Env->>Ninja: parse build.ninja for CUSTOM_COMMAND outputs
    Env->>Ninja: run `ninja -C <build_dir> <missing_targets>`
    Ninja-->>Env: generate missing sources

    SCons->>LDGen: run ldgen -> produce `sections.ld`
    SCons->>Relinker: if `custom_relinker_*` fully set, invoke relinker with `sections.ld`
    Relinker->>Relinker: read CSVs + sdkconfig, run objdump(s), compute include/exclude sections
    Relinker-->>Env: write `sections.ld.relinked` (stamp/target)

    SCons->>Linker: final link depends on `sections.ld.relinked` when present
    Linker-->>Env: produce final ELF/firmware
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I nibble linker lines beneath the moonlit stack,

CSV crumbs and objdump whispers guide each tiny track.
I hop functions from IRAM into flash with care,
stamp a dotted sections.ld.relinked, then vanish through the air.
Hop—build—flash: a rabbit’s tweak, firmware light as air.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature Relinker: move functions from IRAM to Flash' is directly related to the main changeset, which introduces a comprehensive Relinker system that relocates functions from IRAM to Flash memory in ESP32 builds.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch relinker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (11)
builder/relinker/configuration.py (5)

70-81: Improve None comparison style.

Line 92 uses != None which should be is not None per Python style conventions. Additionally, consider using is not None consistently throughout.

♻️ Suggested fix for line 92
     def append(self, func):
         section = self.get_func_section(self.dumps, func)
-        if section != None:
+        if section is not None:
             self.funcs[func] = section
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 70 - 81, Replace any
comparisons using "!= None" with "is not None" in get_func_section and nearby
code to follow Python style; specifically change checks like "if m != None" to
"if m is not None" and use "is not None" consistently for variables such as m
and the result of regex groups, ensuring other None checks (e.g.,
espidf_missing_function_info) already use proper identity comparisons.

56-68: Debug print statement and resource management issues.

  1. Line 61: Debug print('pathes:', pathes) should be removed or converted to logging for production code.
  2. Line 57, 62, 87, 88: Typo pathes should be paths.
  3. Line 67: Use exception chaining with raise ... from e to preserve the original traceback.
♻️ Suggested improvements
 class object_c:
-    def read_dump_info(self, pathes):
+    def read_dump_info(self, paths):
         new_env = os.environ.copy()
         new_env['LC_ALL'] = 'C'
         dumps = list()
-        print('pathes:', pathes)
-        for path in pathes:
+        for path in paths:
             try:
                 dump = StringIO(subprocess.check_output([espidf_objdump, '-t', path], env=new_env).decode())
                 dumps.append(dump.readlines())
             except subprocess.CalledProcessError as e:
-                raise RuntimeError('cmd:%s result:%s'%(e.cmd, e.returncode))
+                raise RuntimeError('cmd:%s result:%s'%(e.cmd, e.returncode)) from e
         return dumps
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 56 - 68, In
object_c.read_dump_info replace the debug print and typo'd variable name: rename
parameter and all uses of "pathes" to "paths", remove the plain print('pathes:',
pathes) and log with the module logger at debug level instead (or remove
entirely), and when re-raising the RuntimeError include exception chaining (use
"raise RuntimeError(... ) from e") so the original CalledProcessError traceback
is preserved; ensure you update the local variable names (e.g., for path in
paths) and the error message construction in the except block to reference the
corrected names.

83-88: Typo in variable and attribute names.

libray should be library for clarity and consistency.

♻️ Suggested fix
-    def __init__(self, name, pathes, libray):
+    def __init__(self, name, paths, library):
         self.name = name
-        self.libray = libray
+        self.library = library
         self.funcs = dict()
-        self.pathes = pathes
-        self.dumps = self.read_dump_info(pathes)
+        self.paths = paths
+        self.dumps = self.read_dump_info(paths)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 83 - 88, The constructor
__init__ uses a misspelled parameter/attribute "libray" — rename the parameter
and attribute to "library" (update the signature parameter 'libray' and all uses
of self.libray to self.library) to keep naming consistent; ensure any other
places in this class that reference the old symbol (e.g., other methods or
initializations) are updated accordingly so there are no unresolved references
to "libray".

160-180: Use context managers for CSV file operations and fix comparison style.

Multiple files are opened without context managers, and line 172 uses == False instead of the idiomatic not operator.

♻️ Suggested improvements
     sdkconfig = sdkconfig_c(sdkconfig_file)

     lib_paths = paths_c()
-    for p in csv.DictReader(open(library_file, 'r')):
-        lib_paths.append(p['library'], '*', p['path'])
+    with open(library_file, 'r', encoding='utf-8') as f:
+        for p in csv.DictReader(f):
+            lib_paths.append(p['library'], '*', p['path'])

     obj_paths = paths_c()
-    for p in csv.DictReader(open(object_file, 'r')):
-        obj_paths.append(p['library'], p['object'], p['path'])
+    with open(object_file, 'r', encoding='utf-8') as f:
+        for p in csv.DictReader(f):
+            obj_paths.append(p['library'], p['object'], p['path'])

     libraries = libraries_c()
-    for d in csv.DictReader(open(function_file, 'r')):
-        if d['option'] and sdkconfig.check(d['option']) == False:
+    with open(function_file, 'r', encoding='utf-8') as f:
+        for d in csv.DictReader(f):
+            if d['option'] and not sdkconfig.check(d['option']):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 160 - 180, Replace plain
open(...) calls with context managers and fix the boolean comparison: open
library_file, object_file, and function_file using with open(..., 'r') as f and
pass f into csv.DictReader to ensure files are closed (change both loops using
csv.DictReader(open(..., 'r')) to with blocks). Also change the condition if
d['option'] and sdkconfig.check(d['option']) == False: to use the idiomatic
check if d['option'] and not sdkconfig.check(d['option']):; keep existing uses
of sdkconfig_c, paths_c, libraries_c, lib_paths.append, obj_paths.append,
libraries.append, lib_paths.index and obj_paths.index when locating paths.

20-29: Use context manager for file operations.

Opening files without a context manager can lead to resource leaks if an exception occurs before the file is closed.

♻️ Suggested fix
 class sdkconfig_c:
     def __init__(self, path):
-        lines = open(path).read().splitlines()
+        with open(path, encoding='utf-8') as f:
+            lines = f.read().splitlines()
         config = dict()
         for l in lines:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 20 - 29, The __init__ of
class sdkconfig_c currently opens the file with open(path).read() without a
context manager; change it to use a with-statement (e.g., with open(path, 'r',
encoding='utf-8') as f: lines = f.read().splitlines()) so the file is always
closed even on exceptions, then proceed to parse lines into the config dict as
before (references: class sdkconfig_c, __init__, path, lines, config).
builder/frameworks/espidf.py (2)

1424-1472: Relinker integration looks good with one minor resource concern.

The relinker integration is well-structured. However, the lambda at lines 1465-1467 opens a file but relies on garbage collection to close it.

♻️ Suggested improvement for proper file handling
                 relinker_step = env.Command(
                     str(Path("$BUILD_DIR") / "sections.ld.relinked"),
                     str(Path("$BUILD_DIR") / "sections.ld"),
                     [
                         env.VerboseAction(_relinker_cmd, "Running relinker to optimize IRAM usage"),
                         # Touch a stamp file so SCons tracks the dependency
                         env.VerboseAction(
-                            lambda target, source, env: open(str(target[0]), 'w').write('done'),
+                            lambda target, source, env: Path(str(target[0])).write_text('done'),
                             ""
                         ),
                     ],
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 1424 - 1472, The lambda used in
the env.VerboseAction for relinker_step writes to the stamp file without closing
it; replace that inline lambda with a small named action/function (e.g.,
write_relinker_stamp) that takes (target, source, env) and uses a with
open(str(target[0]), 'w') as f: f.write('done') pattern to ensure the file is
closed, then pass that function to env.VerboseAction instead of the lambda so
relinker_step (sections.ld.relinked) reliably creates a closed stamp file.

2531-2536: Relinker ELF dependency logic is correct but could use a comment.

The condition checks both file existence and config option because:

  1. On subsequent builds, the stamp file will exist
  2. On first build, we need the config option to create the dependency

This is correct behavior but might be confusing to future maintainers.

📝 Suggested documentation improvement
 # If relinker is configured, ensure the ELF depends on the relinked stamp
+# Check both: (1) stamp exists from previous build, or (2) config option is set for first build
 _relinker_stamp = str(Path(BUILD_DIR) / "sections.ld.relinked")
 if os.path.exists(_relinker_stamp) or config.get(
     "env:" + env["PIOENV"], "custom_relinker_function", ""
 ):
     env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", _relinker_stamp)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 2531 - 2536, Add a short
clarifying comment above the relinker dependency check explaining why both the
stamp file existence and the config option are evaluated: on subsequent builds
the stamp file (_relinker_stamp) will exist and should create the Depends, while
on the very first build the stamp won’t exist so we must check the config key
("env:" + env["PIOENV"], "custom_relinker_function") to ensure the relinker
dependency is registered; keep the existing logic using BUILD_DIR, env and
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", _relinker_stamp) unchanged.
builder/relinker/relinker.py (4)

127-128: Parameter name shadows Python builtin.

The parameter input shadows the Python builtin function. Consider renaming to input_file or linker_input for clarity.

♻️ Suggested fix
 class relink_c:
-    def __init__(self, input, library_file, object_file, function_file, sdkconfig_file, missing_function_info):
-        self.filter = filter_c(input)
+    def __init__(self, input_file, library_file, object_file, function_file, sdkconfig_file, missing_function_info):
+        self.filter = filter_c(input_file)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 127 - 128, The __init__ of class
relink_c currently defines a parameter named input which shadows Python's
builtin; rename that parameter (for example to input_file or linker_input)
throughout the relink_c.__init__ signature and all uses inside the class
(constructor body and any assignments or references) to avoid builtin shadowing
and improve clarity; ensure any external callers that instantiate relink_c are
updated to pass the new parameter name if keyword args are used.

249-252: Use context managers and avoid shadowing builtins.

The save method opens files without context managers and uses input as a parameter name.

♻️ Suggested fix
-    def save(self, input, output):
-        lines = open(input).read().splitlines()
+    def save(self, input_file, output_file):
+        with open(input_file, encoding='utf-8') as f:
+            lines = f.read().splitlines()
         lines = self.__replace__(lines)
-        open(output, 'w+').write('\n'.join(lines))
+        with open(output_file, 'w', encoding='utf-8') as f:
+            f.write('\n'.join(lines))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 249 - 252, In save, avoid
shadowing the builtin and ensure files are opened with context managers: rename
the parameters (e.g., input_path, output_path) and use with open(input_path,
'r', encoding='utf-8') as f: lines = f.read().splitlines(), then lines =
self.__replace__(lines), and write with with open(output_path, 'w',
encoding='utf-8') as f: f.write('\n'.join(lines)); update any callers if needed.

206-247: Inconsistent return type in _replace_func.

The method returns either a modified string, ' ' (space string), or False. Returning False from a function that otherwise returns strings is confusing and can lead to bugs. Consider returning None instead.

♻️ Suggested fix
-        return False
+        return None

And update the caller at line 203-204:

                     new_l = self._replace_func(l)
-                    if new_l:
+                    if new_l is not None:
                         lines[i] = new_l
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 206 - 247, _replace_func currently
returns strings, a space ' ', or the boolean False; change it to return None
instead of False so its return type is either str or None. Update all call sites
that test the result of _replace_func (the caller in the same module that
currently checks for False) to treat None as "no replacement" (use "is None" or
"if result is not None") and avoid relying on truthiness; keep the existing
behavior for returning ' ' and other strings unchanged.

84-86: Use context manager for file operations.

Opening files without a context manager can lead to resource leaks.

♻️ Suggested fix
 class filter_c:
     def __init__(self, file):
-        lines = open(file).read().splitlines()
+        with open(file, encoding='utf-8') as f:
+            lines = f.read().splitlines()
         self.libs_desc = ''
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 84 - 86, The constructor of class
filter_c currently opens the file with open(file) without a context manager (see
filter_c.__init__), which can leak file descriptors; change it to use a with
statement: open the file with "with open(file, 'r', encoding='utf-8') as f:" (or
appropriate encoding), read lines via f.read().splitlines() (or f.readlines() if
preferred) and assign to lines so the file is automatically closed even on
exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/relinker/configuration.py`:
- Around line 205-208: The generator() call in main() is missing the required
missing_function_info parameter; update the call to pass the CLI value (e.g.,
args.missing_function_info) when invoking generator(args.library, args.object,
args.function, args.sdkconfig) and ensure the ArgumentParser defines/uses the
same argument name so missing_function_info is available on args before calling
generator().

In `@builder/relinker/relinker.py`:
- Around line 89-94: The regex string assigned to desc uses backslashes in a
normal string which creates invalid escape sequences; change desc to a raw
string (e.g. desc = r'\(EXCLUDE_FILE\((.*)\) .iram1 ') to avoid escape warnings
and ensure the pattern is interpreted literally, then keep the existing
re.search(desc, l) usage and assignment to self.libs_desc/self.libs unchanged
(optionally check the match is not None before indexing).
- Around line 121-125: In the __str__ method of the ReLinker class, fix the
invalid escape sequence `\l` inside the format string (currently in the string
assigned to s) by replacing it with the intended newline escape `\n` (or use a
multi-line string or join to build the output) so the format becomes valid and
prints each field on its own line; ensure the format string still references
self.lib, self.file, self.lib_path, self.desc, self.secs, self.fsecs, and
self.isecs as before.
- Around line 298-302: The CLI flag defined with
argparser.add_argument('--missing_function_info', default=False, type=bool) is
broken because argparse with type=bool treats string values incorrectly; change
the argument to use action='store_true' (so presence sets it True, default stays
False) for the '--missing_function_info' option in the argparser.add_argument
call (or alternatively implement a custom type/parser), and update any code that
reads the missing_function_info variable to expect a boolean produced by the new
action.

---

Nitpick comments:
In `@builder/frameworks/espidf.py`:
- Around line 1424-1472: The lambda used in the env.VerboseAction for
relinker_step writes to the stamp file without closing it; replace that inline
lambda with a small named action/function (e.g., write_relinker_stamp) that
takes (target, source, env) and uses a with open(str(target[0]), 'w') as f:
f.write('done') pattern to ensure the file is closed, then pass that function to
env.VerboseAction instead of the lambda so relinker_step (sections.ld.relinked)
reliably creates a closed stamp file.
- Around line 2531-2536: Add a short clarifying comment above the relinker
dependency check explaining why both the stamp file existence and the config
option are evaluated: on subsequent builds the stamp file (_relinker_stamp) will
exist and should create the Depends, while on the very first build the stamp
won’t exist so we must check the config key ("env:" + env["PIOENV"],
"custom_relinker_function") to ensure the relinker dependency is registered;
keep the existing logic using BUILD_DIR, env and
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", _relinker_stamp) unchanged.

In `@builder/relinker/configuration.py`:
- Around line 70-81: Replace any comparisons using "!= None" with "is not None"
in get_func_section and nearby code to follow Python style; specifically change
checks like "if m != None" to "if m is not None" and use "is not None"
consistently for variables such as m and the result of regex groups, ensuring
other None checks (e.g., espidf_missing_function_info) already use proper
identity comparisons.
- Around line 56-68: In object_c.read_dump_info replace the debug print and
typo'd variable name: rename parameter and all uses of "pathes" to "paths",
remove the plain print('pathes:', pathes) and log with the module logger at
debug level instead (or remove entirely), and when re-raising the RuntimeError
include exception chaining (use "raise RuntimeError(... ) from e") so the
original CalledProcessError traceback is preserved; ensure you update the local
variable names (e.g., for path in paths) and the error message construction in
the except block to reference the corrected names.
- Around line 83-88: The constructor __init__ uses a misspelled
parameter/attribute "libray" — rename the parameter and attribute to "library"
(update the signature parameter 'libray' and all uses of self.libray to
self.library) to keep naming consistent; ensure any other places in this class
that reference the old symbol (e.g., other methods or initializations) are
updated accordingly so there are no unresolved references to "libray".
- Around line 160-180: Replace plain open(...) calls with context managers and
fix the boolean comparison: open library_file, object_file, and function_file
using with open(..., 'r') as f and pass f into csv.DictReader to ensure files
are closed (change both loops using csv.DictReader(open(..., 'r')) to with
blocks). Also change the condition if d['option'] and
sdkconfig.check(d['option']) == False: to use the idiomatic check if d['option']
and not sdkconfig.check(d['option']):; keep existing uses of sdkconfig_c,
paths_c, libraries_c, lib_paths.append, obj_paths.append, libraries.append,
lib_paths.index and obj_paths.index when locating paths.
- Around line 20-29: The __init__ of class sdkconfig_c currently opens the file
with open(path).read() without a context manager; change it to use a
with-statement (e.g., with open(path, 'r', encoding='utf-8') as f: lines =
f.read().splitlines()) so the file is always closed even on exceptions, then
proceed to parse lines into the config dict as before (references: class
sdkconfig_c, __init__, path, lines, config).

In `@builder/relinker/relinker.py`:
- Around line 127-128: The __init__ of class relink_c currently defines a
parameter named input which shadows Python's builtin; rename that parameter (for
example to input_file or linker_input) throughout the relink_c.__init__
signature and all uses inside the class (constructor body and any assignments or
references) to avoid builtin shadowing and improve clarity; ensure any external
callers that instantiate relink_c are updated to pass the new parameter name if
keyword args are used.
- Around line 249-252: In save, avoid shadowing the builtin and ensure files are
opened with context managers: rename the parameters (e.g., input_path,
output_path) and use with open(input_path, 'r', encoding='utf-8') as f: lines =
f.read().splitlines(), then lines = self.__replace__(lines), and write with with
open(output_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)); update
any callers if needed.
- Around line 206-247: _replace_func currently returns strings, a space ' ', or
the boolean False; change it to return None instead of False so its return type
is either str or None. Update all call sites that test the result of
_replace_func (the caller in the same module that currently checks for False) to
treat None as "no replacement" (use "is None" or "if result is not None") and
avoid relying on truthiness; keep the existing behavior for returning ' ' and
other strings unchanged.
- Around line 84-86: The constructor of class filter_c currently opens the file
with open(file) without a context manager (see filter_c.__init__), which can
leak file descriptors; change it to use a with statement: open the file with
"with open(file, 'r', encoding='utf-8') as f:" (or appropriate encoding), read
lines via f.read().splitlines() (or f.readlines() if preferred) and assign to
lines so the file is automatically closed even on exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f69ea903-f276-44ce-8c5f-a8d76d2c4567

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and 383933e.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (4)
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py

Comment thread builder/relinker/configuration.py
Comment thread builder/relinker/relinker.py
Comment thread builder/relinker/relinker.py Outdated
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/frameworks/espidf.py`:
- Around line 2532-2538: The dependency on the relinker stamp (_relinker_stamp)
is being added whenever only custom_relinker_function is set, but
generate_project_ld_script() only creates sections.ld.relinked when all relinker
options are provided; update the conditional around
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", _relinker_stamp) to mirror
generate_project_ld_script() by requiring either the stamp file exists OR all
three config keys ("custom_relinker_function", "custom_relinker_library",
"custom_relinker_object") are truthy via config.get; this ensures the stamp
dependency is only added when the relinker step is actually configured (use the
same config.get(...) checks and _relinker_stamp variable).
- Around line 1431-1472: The relinker action (relinker_step) only lists
sections.ld as its source, so changes to the relinker script, configuration
files, the three CSV inputs (library/object/function), SDKCONFIG_PATH or any
files under FRAMEWORK_DIR do not trigger rebuilds; update the env.Command call
that creates relinker_step to include the actual file dependencies: the relinker
script (_relinker_script), the relinker module(s) (e.g., configuration.py), the
three CSV files referenced by relinker_library/relinker_object/relinker_function
(resolve their paths and pass them as additional source entries), and
SDKCONFIG_PATH (and any FRAMEWORK_DIR files the relinker reads), or
alternatively call env.Depends(relinker_step, [list_of_these_paths]) so SCons
knows to rebuild sections.ld.relinked when any of those files change.

In `@builder/relinker/configuration.py`:
- Around line 148-153: The index method currently raises KeyError when a library
exists in self.paths but the specific object key is missing; change index (in
builder/relinker/configuration.py) to explicitly check for the object key after
handling the '*' wildcard and return None when obj is not present (i.e., if lib
in self.paths and '*' not in self.paths[lib] and obj not in self.paths[lib] then
return None) so callers can fall back to the library-level path instead of
aborting.

In `@builder/relinker/relinker.py`:
- Around line 21-35: The top-level import "from entity import EntityDB" runs
before CLI parsing so --idf-path can't affect import resolution; move that
import (and any other imports that rely on IDF_PATH) to after parsing in
main()/run_relinker(), call _setup_ldgen_imports(idf_path) with the parsed
idf_path before importing, and set os.environ['IDF_PATH'] = idf_path (do not use
setdefault) so configuration reads the explicit value; update
run_relinker()/main() to pass the parsed idf_path into _setup_ldgen_imports and
only then import EntityDB.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 507a3557-3a40-434f-b951-96957434e570

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and 45d1243.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (4)
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py

Comment thread builder/frameworks/espidf.py Outdated
Comment thread builder/frameworks/espidf.py
Comment thread builder/relinker/configuration.py Outdated
Comment thread builder/relinker/relinker.py
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
builder/frameworks/espidf.py (1)

1595-1623: Skip already-generated outputs before spawning Ninja.

compile_source_files() calls this helper per component, but this loop adds every matching generated target even when abs_path already exists. That keeps incremental builds paying a separate ninja -C ... startup cost for no-op targets.

♻️ Proposed fix
     for source in generated_sources:
         src_path = source["path"]
         if not os.path.isabs(src_path):
             abs_path = str(Path(project_src_dir) / src_path)
         else:
             abs_path = src_path
+        if os.path.exists(abs_path):
+            continue
         # Ninja targets are relative to build_dir, not project_src_dir
         try:
             ninja_target = fs.to_unix_path(
                 str(Path(abs_path).resolve().relative_to(Path(build_dir).resolve()))
             ).lstrip("./")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 1595 - 1623, The loop collects
generated targets even when the generated output file already exists, causing
unnecessary ninja invocations; in the loop inside compile_source_files() that
iterates generated_sources (and builds ninja_target and appends to
generated_targets), check whether the resolved abs_path (or its expected
generated output) already exists on disk (e.g., Path(abs_path).exists()) and
skip adding that target to generated_targets if so; leave the rest
(ninja_custom_targets check, populate_idf_env_vars, exec_command) unchanged so
only missing outputs are passed to exec_command.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/frameworks/espidf.py`:
- Around line 1439-1449: The command string in builder/frameworks/espidf.py
unconditionally appends the relinker flag --missing_function_info (see
_relinker_cmd) which forces "print-and-drop" behavior; change it so the flag is
only added when an explicit opt-in is set (e.g., a new boolean config/option
like relinker_allow_missing_function_info or via existing relinker config), and
consult builder/relinker/configuration.py to honor that setting; update the code
that constructs _relinker_cmd to conditionally append '--missing_function_info'
only if that opt-in flag is true, and ensure tests or callers pass the
new/configured option.

In `@builder/relinker/relinker.py`:
- Around line 204-208: In relinker.py, when you detect '(.stub .gnu.warning' and
insert the flash block into lines[i], set flash_done = True inside that
insertion branch (immediately after the lines[i] = '%s\n\n%s' %
(self.flash_include, l) assignment) so subsequent iterations see that flash has
been handled; this ensures the later elif self.flash_include in l check doesn't
cause duplicate inserts when ldgen emits multiple '(.stub .gnu.warning' markers.

In `@RELINKER_INTEGRATION.md`:
- Around line 18-33: The unlabeled fenced code blocks in RELINKER_INTEGRATION.md
(e.g., the ASCII diagram block starting with the box diagram containing
"sections.ld → ldgen → sections.ld → relinker" and the other blocks around the
comment references) should have explicit language identifiers to satisfy
markdownlint; update each triple-backtick fence to include a language token such
as text, bash, or ini (for example, change ``` to ```text) for the blocks at the
noted locations so the file is lint-clean while keeping the block contents
unchanged.
- Around line 35-39: The docs currently describe function.csv backwards:
function.csv lists functions that are relocated to flash (not kept in IRAM);
builder/relinker/configuration.py only retains rows whose option evaluates to
true and builder/relinker/relinker.py writes those selected rows into
flash_include, so update the text to state that included/true rows go to flash
and omitted/false rows remain in IRAM, and correct the option table meanings for
CONFIG_*, !CONFIG_*, and FALSE accordingly (also apply the same fix in the other
two affected sections around lines 156–165 and 190–197).

---

Nitpick comments:
In `@builder/frameworks/espidf.py`:
- Around line 1595-1623: The loop collects generated targets even when the
generated output file already exists, causing unnecessary ninja invocations; in
the loop inside compile_source_files() that iterates generated_sources (and
builds ninja_target and appends to generated_targets), check whether the
resolved abs_path (or its expected generated output) already exists on disk
(e.g., Path(abs_path).exists()) and skip adding that target to generated_targets
if so; leave the rest (ninja_custom_targets check, populate_idf_env_vars,
exec_command) unchanged so only missing outputs are passed to exec_command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e2eabf5-8a4c-45b4-b0c8-65af02a0fa53

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and 940fcfe.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (5)
  • RELINKER_INTEGRATION.md
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py

Comment thread builder/frameworks/espidf.py Outdated
Comment thread RELINKER_INTEGRATION.md Outdated
Comment thread RELINKER_INTEGRATION.md Outdated
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
builder/frameworks/espidf.py (1)

1439-1462: ⚠️ Potential issue | 🟠 Major

Keep missing-symbol tolerance opt-in.

builder/relinker/configuration.py drops rows when section lookup fails in this mode, so defaulting this flag to "yes" lets typos or IDF drift silently produce a partially relinked sections.ld. Please default it to off and only append --missing_function_info when the user explicitly enables it.

🛠️ Proposed fix
             _relinker_missing = config.get(
-                "env:" + env["PIOENV"], "custom_relinker_missing_function_info", "yes"
+                "env:" + env["PIOENV"], "custom_relinker_missing_function_info", "no"
             ).strip().lower() in ("yes", "true", "1")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 1439 - 1462, The code currently
defaults _relinker_missing to true by using config.get(..., "yes"); change the
default to off so missing-symbol tolerance is opt-in: update the config.get call
for "custom_relinker_missing_function_info" to default to "no"/"false"/"0" (or
otherwise parse absence as False) and keep the existing logic that only appends
' --missing_function_info' to _relinker_cmd when _relinker_missing is True;
modify the _relinker_missing assignment (the variable and the config key
"custom_relinker_missing_function_info") so typos/IDF drift do not silently
enable the flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@RELINKER_INTEGRATION.md`:
- Around line 277-288: The guidance is too broad—change the ISR wording to match
the earlier minimal examples by limiting "must stay in IRAM" to code paths that
run with flash cache disabled or that are explicitly IRAM-safe (e.g., early-boot
handlers, flash callbacks like spi_flash_guard_* / spi_flash_os_*, cache error
handlers, RTC/sleep callbacks, and functions marked IRAM_ATTR) and avoid blanket
statements that every function "called from ISR" must be in IRAM; explicitly
call out exceptions such as FreeRTOS ISR helpers that are IRAM-safe (e.g.,
xQueueGiveFromISR) and add a short sentence instructing authors to verify
whether their interrupt runs with flash cache disabled before moving functions
to flash.

---

Duplicate comments:
In `@builder/frameworks/espidf.py`:
- Around line 1439-1462: The code currently defaults _relinker_missing to true
by using config.get(..., "yes"); change the default to off so missing-symbol
tolerance is opt-in: update the config.get call for
"custom_relinker_missing_function_info" to default to "no"/"false"/"0" (or
otherwise parse absence as False) and keep the existing logic that only appends
' --missing_function_info' to _relinker_cmd when _relinker_missing is True;
modify the _relinker_missing assignment (the variable and the config key
"custom_relinker_missing_function_info") so typos/IDF drift do not silently
enable the flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07b916cc-78c0-49ee-9bba-3e42d3c76c0e

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and 879a680.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (5)
  • RELINKER_INTEGRATION.md
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py

Comment thread RELINKER_INTEGRATION.md
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
builder/relinker/configuration.py (1)

153-160: ⚠️ Potential issue | 🟠 Major

Prefer the explicit object mapping before falling back to *.

If a library has both an object-specific path and a wildcard path, this method always rewrites the lookup to *, so the override is unreachable. That makes mixed wildcard/override CSVs resolve the wrong file.

🐛 Proposed fix
     def index(self, lib, obj):
         if lib not in self.paths:
             return None
-        if '*' in self.paths[lib]:
-            obj = '*'
-        if obj not in self.paths[lib]:
-            return None
-        return self.paths[lib][obj]
+        if obj in self.paths[lib]:
+            return self.paths[lib][obj]
+        if '*' in self.paths[lib]:
+            return self.paths[lib]['*']
+        return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 153 - 160, The index method
currently prefers the wildcard mapping ('*') before checking for an explicit
object mapping, causing explicit overrides to be ignored; change the lookup
order in index(lib, obj) to first check if lib exists in self.paths, then if obj
is explicitly present in self.paths[lib] and return that, and only if the
explicit obj is missing fall back to checking for '*' in self.paths[lib] and
return the wildcard entry; ensure the existing None returns remain for missing
lib or missing both explicit and wildcard entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/frameworks/espidf.py`:
- Around line 1599-1629: In _ensure_generated_sources(), stop skipping generated
sources that already exist: remove the existence check (the if
os.path.exists(abs_path): continue) so that all entries from generated_sources
that map to ninja_custom_targets are appended to generated_targets every run;
this ensures all ninja targets in all_targets are passed to exec_command (the
ninja invocation) so Ninja can decide whether to rebuild based on its inputs/dep
tracking rather than SCons short-circuiting. Keep the surrounding logic that
computes ninja_target and checks ninja_custom_targets and ensure
generated_targets is still used to build all_targets for exec_command.

In `@builder/relinker/relinker.py`:
- Around line 346-348: The run_relinker function's default for
missing_function_info (currently True) causes silent dropping of missing symbols
and diverges from the CLI's fail-fast behavior; change the default parameter in
run_relinker(...) from missing_function_info=True to missing_function_info=False
so the API fails fast like main(), and verify callers that relied on the old
default explicitly pass True if they need the prior silent behavior (look for
references to run_relinker to update callers as needed).
- Around line 137-140: The call to configuration.generator in relinker.__init__
omits the real build directory so relative CSV paths are resolved incorrectly;
update relinker.__init__ (constructor) to accept a build_dir argument (or obtain
the real build dir from the caller/context) and pass that build_dir into
configuration.generator when calling it (preserving existing params:
library_file, object_file, function_file, sdkconfig_file, missing_function_info,
espidf_objdump), and then update any instantiations of the Relinker class to
supply the real build_dir so configuration.generator can resolve CSV-relative
paths against BUILD_DIR.

In `@RELINKER_INTEGRATION.md`:
- Around line 47-59: The example uses POSIX-only paths and shell commands;
update RELINKER_INTEGRATION.md to be OS-neutral by either (A) prefixing the
existing snippet with a clear "POSIX (Linux/macOS) example" label, or (B) adding
a Windows PowerShell alternative that uses the user's profile path (e.g.,
%USERPROFILE% or $env:USERPROFILE) and the PowerShell equivalents of creating a
directory and copying files (New-Item / Copy-Item), and explicitly reference the
same platformio example directory under the user's platform folder so Windows
users know where to copy from; ensure both options mention the relinker/ target
directory and the example source path.

---

Duplicate comments:
In `@builder/relinker/configuration.py`:
- Around line 153-160: The index method currently prefers the wildcard mapping
('*') before checking for an explicit object mapping, causing explicit overrides
to be ignored; change the lookup order in index(lib, obj) to first check if lib
exists in self.paths, then if obj is explicitly present in self.paths[lib] and
return that, and only if the explicit obj is missing fall back to checking for
'*' in self.paths[lib] and return the wildcard entry; ensure the existing None
returns remain for missing lib or missing both explicit and wildcard entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43110e62-d887-423c-89f9-419cfd5f377d

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and ad766ed.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (5)
  • RELINKER_INTEGRATION.md
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py

Comment thread builder/frameworks/espidf.py
Comment thread builder/relinker/relinker.py Outdated
Comment thread builder/relinker/relinker.py
Comment thread RELINKER_INTEGRATION.md
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
test/TESTING.md (1)

224-227: ⚠️ Potential issue | 🟡 Minor

Cleanup instructions don’t match the temp directory pattern shown in the same doc.

tempfile.mkdtemp() at Line 226 typically creates /tmp/tmp*, but Line 333 suggests /tmp/tmp*relinker*, which usually won’t match anything. Either use a dedicated prefix in examples or adjust cleanup guidance to exact printed temp paths.

🛠️ Suggested doc alignment
-        self.temp_dir = tempfile.mkdtemp()
+        self.temp_dir = tempfile.mkdtemp(prefix="relinker_test_")
...
-rm -rf /tmp/tmp*relinker*
+rm -rf /tmp/relinker_test_*

Also applies to: 325-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/TESTING.md` around lines 224 - 227, The cleanup guidance and example
temp paths don't match how the temp dir is created in setUp: tempfile.mkdtemp()
produces names like /tmp/tmp* but the doc's cleanup examples expect
/tmp/tmp*relinker*; to fix, either create the temp dir with a deterministic
prefix (e.g., call tempfile.mkdtemp(prefix="relinker_") where setUp assigns
self.temp_dir) or update the cleanup instructions/examples to reference the
actual pattern produced by tempfile.mkdtemp() (e.g., /tmp/tmp* or use
self.temp_dir directly) so the documented cleanup matches the code.
test/test_relinker_functionality.py (1)

54-92: ⚠️ Potential issue | 🟡 Minor

Restore IDF_PATH from the outer cleanup.

Any failure before the inner finally leaks the modified process environment into later tests. Move the restore alongside shutil.rmtree() so it always runs.

♻️ Safer cleanup
     def test_path_normalization(self):
         """Test path normalization and resolution."""
         temp_dir = tempfile.mkdtemp()
+        original_idf = os.environ.get('IDF_PATH')
         try:
-            # Set IDF_PATH
-            original_idf = os.environ.get('IDF_PATH')
             os.environ['IDF_PATH'] = '/test/esp-idf'
@@
-            finally:
-                # Restore IDF_PATH
-                if original_idf:
-                    os.environ['IDF_PATH'] = original_idf
         finally:
+            if original_idf is not None:
+                os.environ['IDF_PATH'] = original_idf
+            else:
+                os.environ.pop('IDF_PATH', None)
             shutil.rmtree(temp_dir)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_relinker_functionality.py` around lines 54 - 92, The test modifies
the environment variable IDF_PATH inside test_path_normalization but only
restores it in an inner finally block, which can leak into other tests; move the
restoration of the original_idf so it always executes in the outer finally (the
same block that calls shutil.rmtree(temp_dir)) to guarantee IDF_PATH is restored
even if an earlier assertion or exception occurs; update the cleanup logic in
test_path_normalization to set os.environ['IDF_PATH'] back to original_idf (or
delete it if original_idf was None) inside the outer finally that runs after
tempfile.mkdtemp()/shutil.rmtree.
test/test_configuration.py (1)

209-217: ⚠️ Potential issue | 🟡 Minor

Pin espidf_missing_function_info inside this test.

object_c.append() only returns False in warning-only mode, and configuration.generator() mutates that module-global. If another test flips it to False first, this starts raising RuntimeError instead of returning False.

🧪 Stabilize the test
-        with mock.patch.object(object_c, 'read_dump_info', return_value=[['mock dump data']]), \
+        with mock.patch('configuration.espidf_missing_function_info', True), \
+             mock.patch.object(object_c, 'read_dump_info', return_value=[['mock dump data']]), \
              mock.patch.object(object_c, 'get_func_section', return_value=None):
             obj = object_c('test.c.obj', [], 'libtest.a')
             result = obj.append('nonexistent_function')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_configuration.py` around lines 209 - 217, The test can be flaky
because object_c.append() behavior depends on the module-global
espidf_missing_function_info mutated by configuration.generator(); to stabilize,
explicitly pin espidf_missing_function_info to the warning-only value inside the
test (e.g. use mock.patch.object or monkeypatch to set
configuration.espidf_missing_function_info = True) before constructing object_c
and calling obj.append('nonexistent_function'), so the test always exercises the
"append returns False" branch regardless of other tests mutating
configuration.generator().
builder/frameworks/espidf.py (1)

2357-2369: ⚠️ Potential issue | 🟡 Minor

Treat a non-runnable venv Python as broken too.

The new validation only checks that the interpreter file exists. A corrupted or non-executable binary still passes _is_venv_interpreter_valid(), then _get_idf_venv_python_version() raises OSError and the build aborts instead of recreating the IDF venv.

🛠️ Harden the validation path
     def _get_idf_venv_python_version():
         try:
             version = subprocess.check_output(
                 [
                     get_python_exe(),
                     "-c",
                     "import sys;print('{0}.{1}.{2}-{3}.{4}'.format(*list(sys.version_info)))"
                 ], text=True
             )
             return version.strip()
-        except subprocess.CalledProcessError as e:
+        except (subprocess.CalledProcessError, OSError) as e:
             print("Failed to extract Python version from IDF virtual env!")
             return None
@@
     def _is_venv_interpreter_valid(venv_dir):
         python_path = get_executable_path(venv_dir, "python")
-        return os.path.isfile(python_path)
+        if not os.path.isfile(python_path):
+            return False
+        try:
+            subprocess.check_output(
+                [python_path, "-c", "import sys"],
+                stderr=subprocess.STDOUT,
+                text=True,
+            )
+            return True
+        except (subprocess.CalledProcessError, OSError):
+            return False

Also applies to: 2431-2457

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 2357 - 2369, The validation
currently only checks for the interpreter file's existence; update
_is_venv_interpreter_valid() to ensure the venv python is runnable (executable
and actually launches) by attempting to execute a lightweight command (e.g.,
get_python_exe() with "-c 'import sys; print(1)'" or equivalent) and treat any
OSError/CalledProcessError/ non-zero exit as invalid so the code path will
recreate the IDF venv instead of letting _get_idf_venv_python_version() raise;
ensure error cases are handled consistently and that
_get_idf_venv_python_version() remains tolerant (or its call guarded) to avoid
unhandled OSError if the interpreter is invalid.
🧹 Nitpick comments (2)
test/test_integration.py (1)

302-321: Test class validates fixture setup rather than relinker behavior.

These tests verify that the setUp() fixture is written correctly, but don't exercise the actual linker script modification code. The relinker transformation testing is handled by TestIdempotency, so this isn't a gap in coverage, but the class name TestLinkerScriptModification is misleading.

Consider renaming to TestLinkerScriptFixture or adding actual relink transformation assertions here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_integration.py` around lines 302 - 321, The test class currently
named TestLinkerScriptModification only exercises the setUp fixture (methods
test_linker_script_exists, test_linker_script_has_iram_section,
test_linker_script_has_flash_section) so rename the class to
TestLinkerScriptFixture to reflect that it validates fixture setup (or
alternatively add assertions that call the actual relinker/transformation code
used by TestIdempotency if you prefer to keep the original name); update the
class declaration from TestLinkerScriptModification to TestLinkerScriptFixture
and adjust any references/imports or docstrings accordingly.
builder/relinker/relinker.py (1)

220-252: Pattern-based detection may still match non-relinker lines in edge cases.

The _is_relinker_iram_include() and _is_relinker_flash_include() predicates use heuristic pattern matching rather than comparing against exact generated lines. While the patterns are reasonably specific (requiring *, :, .*( in combination), they could theoretically match hand-written ldgen entries.

However, based on learnings, PlatformIO regenerates sections.ld when relinker settings change, so this is unlikely to cause issues in practice. Consider this a known limitation rather than a blocking issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 220 - 252, The current heuristics
in _is_relinker_iram_include and _is_relinker_flash_include can false-match
hand-written lines; tighten detection by switching to anchored, specific regex
checks: in _is_relinker_iram_include require the line to start with '*' then a
lib:obj token (no spaces), include the literal substring '.*(' immediately
followed by section tokens containing '.iram1.' and a closing ')' (i.e. match
the full pattern rather than substrings); similarly in
_is_relinker_flash_include require an anchored pattern that contains explicit
section names ('.literal.' or '.text.' or '.iram1.') inside the parentheses and
no extraneous characters outside the expected lib:obj.*(...) form; update these
two functions to use those stricter matches so only relinker-generated lines are
detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/test_integration.py`:
- Around line 464-465: The test is calling the relink_c constructor with
arguments in the wrong order; change the call to pass input_script (the linker
script) as the first argument followed by library_csv, object_csv, function_csv,
sdkconfig (and keep missing_function_info=True) so the invocation matches
relink_c.__init__(self, input, library_file, object_file, function_file,
sdkconfig_file, ...); update the line that creates relink1 to use input_script
as the first parameter and the other CSV variables in the original order.
- Around line 474-475: The second call to relink_c uses the wrong argument
order; update the invocation of relink_c (the call assigning relink2) to match
the function's expected parameter order by rearranging the passed variables
(library_csv, object_csv, function_csv, output1, sdkconfig,
missing_function_info=True) into the correct sequence used by the relink_c
definition so that the positional arguments align with the function signature
rather than the current incorrect order.

---

Duplicate comments:
In `@builder/frameworks/espidf.py`:
- Around line 2357-2369: The validation currently only checks for the
interpreter file's existence; update _is_venv_interpreter_valid() to ensure the
venv python is runnable (executable and actually launches) by attempting to
execute a lightweight command (e.g., get_python_exe() with "-c 'import sys;
print(1)'" or equivalent) and treat any OSError/CalledProcessError/ non-zero
exit as invalid so the code path will recreate the IDF venv instead of letting
_get_idf_venv_python_version() raise; ensure error cases are handled
consistently and that _get_idf_venv_python_version() remains tolerant (or its
call guarded) to avoid unhandled OSError if the interpreter is invalid.

In `@test/test_configuration.py`:
- Around line 209-217: The test can be flaky because object_c.append() behavior
depends on the module-global espidf_missing_function_info mutated by
configuration.generator(); to stabilize, explicitly pin
espidf_missing_function_info to the warning-only value inside the test (e.g. use
mock.patch.object or monkeypatch to set
configuration.espidf_missing_function_info = True) before constructing object_c
and calling obj.append('nonexistent_function'), so the test always exercises the
"append returns False" branch regardless of other tests mutating
configuration.generator().

In `@test/test_relinker_functionality.py`:
- Around line 54-92: The test modifies the environment variable IDF_PATH inside
test_path_normalization but only restores it in an inner finally block, which
can leak into other tests; move the restoration of the original_idf so it always
executes in the outer finally (the same block that calls
shutil.rmtree(temp_dir)) to guarantee IDF_PATH is restored even if an earlier
assertion or exception occurs; update the cleanup logic in
test_path_normalization to set os.environ['IDF_PATH'] back to original_idf (or
delete it if original_idf was None) inside the outer finally that runs after
tempfile.mkdtemp()/shutil.rmtree.

In `@test/TESTING.md`:
- Around line 224-227: The cleanup guidance and example temp paths don't match
how the temp dir is created in setUp: tempfile.mkdtemp() produces names like
/tmp/tmp* but the doc's cleanup examples expect /tmp/tmp*relinker*; to fix,
either create the temp dir with a deterministic prefix (e.g., call
tempfile.mkdtemp(prefix="relinker_") where setUp assigns self.temp_dir) or
update the cleanup instructions/examples to reference the actual pattern
produced by tempfile.mkdtemp() (e.g., /tmp/tmp* or use self.temp_dir directly)
so the documented cleanup matches the code.

---

Nitpick comments:
In `@builder/relinker/relinker.py`:
- Around line 220-252: The current heuristics in _is_relinker_iram_include and
_is_relinker_flash_include can false-match hand-written lines; tighten detection
by switching to anchored, specific regex checks: in _is_relinker_iram_include
require the line to start with '*' then a lib:obj token (no spaces), include the
literal substring '.*(' immediately followed by section tokens containing
'.iram1.' and a closing ')' (i.e. match the full pattern rather than
substrings); similarly in _is_relinker_flash_include require an anchored pattern
that contains explicit section names ('.literal.' or '.text.' or '.iram1.')
inside the parentheses and no extraneous characters outside the expected
lib:obj.*(...) form; update these two functions to use those stricter matches so
only relinker-generated lines are detected.

In `@test/test_integration.py`:
- Around line 302-321: The test class currently named
TestLinkerScriptModification only exercises the setUp fixture (methods
test_linker_script_exists, test_linker_script_has_iram_section,
test_linker_script_has_flash_section) so rename the class to
TestLinkerScriptFixture to reflect that it validates fixture setup (or
alternatively add assertions that call the actual relinker/transformation code
used by TestIdempotency if you prefer to keep the original name); update the
class declaration from TestLinkerScriptModification to TestLinkerScriptFixture
and adjust any references/imports or docstrings accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b5651aa-db43-466c-87f0-f69b052c9f06

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and b334ee9.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (14)
  • RELINKER_INTEGRATION.md
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py
  • test/.gitignore
  • test/README.md
  • test/TESTING.md
  • test/VALIDATION_REPORT.md
  • test/run_tests.py
  • test/test_configuration.py
  • test/test_integration.py
  • test/test_relinker.py
  • test/test_relinker_functionality.py

Comment thread test/test_integration.py Outdated
Comment thread test/test_integration.py Outdated
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
builder/relinker/configuration.py (1)

109-127: ⚠️ Potential issue | 🟠 Major

Strict mode still fails open here.

--missing_function_info is documented as “print missing function info instead of raising”, but both failure branches return False even when that flag is off. In the default builder/frameworks/espidf.py integration path, that silently drops relink targets instead of failing the build.

Suggested fix
     def append(self, func):
         if func in self._WILDCARD_SECTIONS:
             self.funcs[func] = self._WILDCARD_SECTIONS[func]
             self.section_all = True
             return True
         if not self.dumps:
-            # Return False silently regardless of espidf_missing_function_info setting
-            # Only print warning if espidf_missing_function_info is True
             if espidf_missing_function_info:
                 print("Warning: Failed to load symbol data for %s:%s" % (self.library, self.name))
-            return False
+                return False
+            raise RuntimeError(
+                "Failed to load symbol data for %s:%s" % (self.library, self.name)
+            )
         section = self.get_func_section(self.dumps, func)
         if section is None:
-            # Return False silently regardless of espidf_missing_function_info setting
-            # Only print warning if espidf_missing_function_info is True
             if espidf_missing_function_info:
                 print("Warning: Failed to find section for function '%s' in %s:%s"
                       % (func, self.library, self.name))
-            return False
+                return False
+            raise RuntimeError(
+                "Failed to find section for function '%s' in %s:%s"
+                % (func, self.library, self.name)
+            )
         self.funcs[func] = section
         return True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 109 - 127, The append method
in configuration.py incorrectly "fails open": when espidf_missing_function_info
is False it still returns False on both missing dumps and missing section
instead of raising, which causes silent drops; update append (function name
append, checks of self.dumps and get_func_section) to raise a clear exception
(e.g., RuntimeError or a custom exception) with contextual message including
self.library, self.name and func when espidf_missing_function_info is False,
while keeping the current print-and-return behavior when
espidf_missing_function_info is True so callers see warnings in permissive mode.
test/test_relinker_functionality.py (1)

158-229: ⚠️ Potential issue | 🟠 Major

This generator test never reaches the objdump/symbol path.

builder/relinker/configuration.py:object_c.read_dump_info() returns before calling subprocess.check_output() unless the object file exists. Because this fixture never creates temp_dir/esp-idf/test.c.obj, the mock stays unused and the test still passes even if section extraction regresses.

Suggested fix
             sdkconfig = os.path.join(temp_dir, 'sdkconfig')
             with open(sdkconfig, 'w') as f:
                 f.write('CONFIG_TEST=y\n')
+
+            obj_path = os.path.join(temp_dir, 'esp-idf', 'test.c.obj')
+            os.makedirs(os.path.dirname(obj_path), exist_ok=True)
+            with open(obj_path, 'wb'):
+                pass
@@
             with patch('configuration.subprocess.check_output') as mock_subprocess:
                 # Mock objdump output for symbol table
                 mock_subprocess.return_value = b'00000000 g F .text.test_func 00000010 test_func\n'
@@
-                # Verify the object was processed (may or may not be present depending on file existence)
+                mock_subprocess.assert_called_once()
                 lib = libraries.libs['libtest.a']
                 self.assertEqual(lib.name, 'libtest.a', "Library name should match")
                 self.assertTrue(hasattr(lib, 'objs'), "Library should have objs attribute")
+                self.assertIn('test.c.obj', lib.objs)
+                self.assertEqual(
+                    lib.objs['test.c.obj'].funcs['test_func'],
+                    'test_func',
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_relinker_functionality.py` around lines 158 - 229, The test never
exercises object_c.read_dump_info's call to subprocess.check_output because the
object file path (temp_dir/esp-idf/test.c.obj) doesn't exist; modify
test_csv_processing so before calling generator it either creates the expected
object file at esp-idf/test.c.obj (e.g., mkdir esp-idf and touch a minimal file)
or patches os.path.exists (or object_c.read_dump_info) to return True so that
subprocess.check_output (patched as configuration.subprocess.check_output) is
actually invoked and the objdump/symbol path is exercised.
builder/relinker/relinker.py (2)

349-357: ⚠️ Potential issue | 🔴 Critical

Don't erase the bare .iram1 catch-all on flash-only relinks.

Line 357 sets self.iram1_exclude to '' when there is nothing to exclude, but Lines 391-395 still replace the original *(.iram1 .iram1.*) descriptor with that empty value. For the common case where func2sect() only produced .literal/.text targets, this drops the global .iram1 mapping and can leave unrelated IRAM sections unmapped.

🛠️ Minimal fix
         self.iram1_exclude = (
             '    *(EXCLUDE_FILE(%s) .iram1.*)\n'
             '    *(EXCLUDE_FILE(%s) .iram1)'
-        ) % (exclude_tokens, exclude_tokens) if exclude_tokens else ''
+        ) % (exclude_tokens, exclude_tokens) if exclude_tokens else '    *(.iram1 .iram1.*)'

Also applies to: 388-395

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 349 - 357, The current code sets
self.iram1_exclude to '' when exclude_tokens is empty, which causes the original
catch-all mapping for .iram1 to be removed during the later replacement;
instead, when exclude_tokens is empty set self.iram1_exclude to the original
catch-all mapping string (e.g. '    *(.iram1 .iram1.*)') or alter the
replacement logic so that when exclude_tokens == '' you skip replacing the
original '*(.iram1 .iram1.*)' entry; update the code that builds
self.iram1_exclude (the expression using exclude_tokens, self.filter.add(), and
iram1_exclude) so it preserves the global .iram1 mapping when there are no
exclude tokens.

220-251: ⚠️ Potential issue | 🟠 Major

The cleanup pass can still strip ldgen-owned include lines.

Lines 220-251 treat any *lib:obj.*(...) mapping as relinker-owned, and Lines 402-417 delete those matches wholesale around the IRAM/flash markers. ldgen emits the same shape for its own object-specific placements, so a first relink pass can remove legitimate mappings before inserting the new block.

🛠️ Safer cleanup approach
+        relinker_iram_lines = {
+            line.strip() for line in self.iram1_include.splitlines() if line.strip()
+        }
+        relinker_flash_lines = {
+            line.strip() for line in self.flash_include.splitlines() if line.strip()
+        }
...
-                    while j < len(lines) and _is_relinker_iram_include(lines[j]):
+                    while j < len(lines) and lines[j].strip() in relinker_iram_lines:
                         lines.pop(j)
...
-                    while j >= 0 and (_is_relinker_flash_include(lines[j]) or not lines[j].strip()):
-                        if _is_relinker_flash_include(lines[j]):
+                    while j >= 0 and (lines[j].strip() in relinker_flash_lines or not lines[j].strip()):
+                        if lines[j].strip() in relinker_flash_lines:
                             lines.pop(j)

Also applies to: 396-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/relinker.py` around lines 220 - 251, The cleanup functions
_is_relinker_iram_include and _is_relinker_flash_include are too permissive and
match ldgen's object-specific includes; tighten detection to only target
relinker-owned lines by requiring a unique relinker marker/token in the line
(e.g. "/* RELINKER */" or another deterministic token) before returning True,
and update the relinker code that emits its include lines to append that token;
ensure the deletion logic that calls these predicates (the cleanup around the
IRAM/flash markers referenced near the existing deletion blocks) uses the new
stricter checks so ldgen-owned "*lib:obj.*(...)" entries are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/frameworks/espidf.py`:
- Around line 2435-2457: The current flow only calls install_python_deps inside
_recreate_and_save, so when a venv exists and _is_venv_outdated returns False
the code skips validating/installing packages; change the steady-state path to
still call install_python_deps(deps) to ensure packages are present even when
the metadata matches. Concretely, after computing deps = _get_python_deps() and
verifying venv_data_file exists and
_is_venv_interpreter_valid(get_idf_venv_dir()), call install_python_deps(deps)
(but do not recreate the venv) before exiting; keep _recreate_and_save(venv_dir,
deps, venv_data_file) for the recreate cases and remove the early-return that
skips package installation. Ensure references to _recreate_and_save,
install_python_deps, _get_python_deps, _is_venv_interpreter_valid, and
_is_venv_outdated are updated accordingly.

In `@builder/relinker/relinker.py`:
- Around line 153-168: The match method currently compares full descriptors like
"libfreertos.a:tasks.*" against entries that only contain whole-library names,
so the check is always false; update match(self, desc) to extract the library
token first (base = desc.split(':', 1)[0].lstrip('*')) and then test if base in
self.entries, preserving the original printed descriptor when removing; apply
the identical change to the similar method/block around lines 271-278 to ensure
library-level exclusions are honored.

---

Duplicate comments:
In `@builder/relinker/configuration.py`:
- Around line 109-127: The append method in configuration.py incorrectly "fails
open": when espidf_missing_function_info is False it still returns False on both
missing dumps and missing section instead of raising, which causes silent drops;
update append (function name append, checks of self.dumps and get_func_section)
to raise a clear exception (e.g., RuntimeError or a custom exception) with
contextual message including self.library, self.name and func when
espidf_missing_function_info is False, while keeping the current
print-and-return behavior when espidf_missing_function_info is True so callers
see warnings in permissive mode.

In `@builder/relinker/relinker.py`:
- Around line 349-357: The current code sets self.iram1_exclude to '' when
exclude_tokens is empty, which causes the original catch-all mapping for .iram1
to be removed during the later replacement; instead, when exclude_tokens is
empty set self.iram1_exclude to the original catch-all mapping string (e.g. '   
*(.iram1 .iram1.*)') or alter the replacement logic so that when exclude_tokens
== '' you skip replacing the original '*(.iram1 .iram1.*)' entry; update the
code that builds self.iram1_exclude (the expression using exclude_tokens,
self.filter.add(), and iram1_exclude) so it preserves the global .iram1 mapping
when there are no exclude tokens.
- Around line 220-251: The cleanup functions _is_relinker_iram_include and
_is_relinker_flash_include are too permissive and match ldgen's object-specific
includes; tighten detection to only target relinker-owned lines by requiring a
unique relinker marker/token in the line (e.g. "/* RELINKER */" or another
deterministic token) before returning True, and update the relinker code that
emits its include lines to append that token; ensure the deletion logic that
calls these predicates (the cleanup around the IRAM/flash markers referenced
near the existing deletion blocks) uses the new stricter checks so ldgen-owned
"*lib:obj.*(...)" entries are preserved.

In `@test/test_relinker_functionality.py`:
- Around line 158-229: The test never exercises object_c.read_dump_info's call
to subprocess.check_output because the object file path
(temp_dir/esp-idf/test.c.obj) doesn't exist; modify test_csv_processing so
before calling generator it either creates the expected object file at
esp-idf/test.c.obj (e.g., mkdir esp-idf and touch a minimal file) or patches
os.path.exists (or object_c.read_dump_info) to return True so that
subprocess.check_output (patched as configuration.subprocess.check_output) is
actually invoked and the objdump/symbol path is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7eb50f39-d43e-4658-8da6-49afed1b5a18

📥 Commits

Reviewing files that changed from the base of the PR and between b800765 and 8812031.

⛔ Files ignored due to path filters (3)
  • builder/relinker/examples/esp32c2/function.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/library.csv is excluded by !**/*.csv
  • builder/relinker/examples/esp32c2/object.csv is excluded by !**/*.csv
📒 Files selected for processing (14)
  • RELINKER_INTEGRATION.md
  • builder/frameworks/espidf.py
  • builder/relinker/__init__.py
  • builder/relinker/configuration.py
  • builder/relinker/relinker.py
  • test/.gitignore
  • test/README.md
  • test/TESTING.md
  • test/VALIDATION_REPORT.md
  • test/run_tests.py
  • test/test_configuration.py
  • test/test_integration.py
  • test/test_relinker.py
  • test/test_relinker_functionality.py

Comment thread builder/frameworks/espidf.py
Comment thread builder/relinker/relinker.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
builder/frameworks/espidf.py (1)

2446-2452: ⚠️ Potential issue | 🟠 Major

Steady-state IDF venvs never reconcile missing packages.

If the metadata still matches, this path exits without calling install_python_deps(). A partially broken venv then survives until a later import/build failure, even though install_python_deps() is already idempotent and only repairs missing/mismatched packages.

🐛 Proposed fix
-    if not os.path.isfile(venv_data_file):
-        _recreate_and_save(venv_dir, deps, venv_data_file)
-    elif not _is_venv_interpreter_valid(venv_dir):
+    recreate = False
+    if not os.path.isfile(venv_data_file):
+        recreate = True
+    elif not _is_venv_interpreter_valid(venv_dir):
         print("Warning! Python interpreter in the IDF virtual environment is missing. Recreating...")
-        _recreate_and_save(venv_dir, deps, venv_data_file)
+        recreate = True
     elif _is_venv_outdated(venv_data_file, deps):
-        _recreate_and_save(venv_dir, deps, venv_data_file)
+        recreate = True
+
+    if recreate:
+        _recreate_and_save(venv_dir, deps, venv_data_file)
+    else:
+        install_python_deps(deps)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/espidf.py` around lines 2446 - 2452, The current flow
skips repair when venv metadata matches, allowing missing packages to persist;
update the logic in the block that checks
venv_data_file/_is_venv_interpreter_valid/_is_venv_outdated so that when the
metadata matches (i.e., none of those conditions triggered) you still call
install_python_deps(venv_dir, deps) to reconcile/install any missing or
mismatched packages instead of exiting early; specifically, locate the
conditional around venv_data_file/venv_dir and after the final elif branch add a
call to install_python_deps(venv_dir, deps) (or replace the no-op path with that
call) while keeping existing calls to _recreate_and_save(...) when interpreter
is missing or venv is outdated.
builder/relinker/configuration.py (1)

113-126: ⚠️ Potential issue | 🟠 Major

Strict mode still drops missing relinker targets silently.

Line 279 says --missing_function_info should switch from raising to printing, but these branches return False in both modes. That makes library_c.append() quietly omit stale/missing entries in the default strict path instead of failing fast.

🐛 Proposed fix
         if not self.dumps:
-            # Return False silently regardless of espidf_missing_function_info setting
-            # Only print warning if espidf_missing_function_info is True
             if espidf_missing_function_info:
                 print("Warning: Failed to load symbol data for %s:%s" % (self.library, self.name))
-            return False
+                return False
+            raise RuntimeError(
+                "Failed to load symbol data for %s:%s" % (self.library, self.name)
+            )
         section = self.get_func_section(self.dumps, func)
         if section is None:
-            # Return False silently regardless of espidf_missing_function_info setting
-            # Only print warning if espidf_missing_function_info is True
             if espidf_missing_function_info:
                 print("Warning: Failed to find section for function '%s' in %s:%s"
                       % (func, self.library, self.name))
-            return False
+                return False
+            raise RuntimeError(
+                "Failed to find section for function '%s' in %s:%s"
+                % (func, self.library, self.name)
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/relinker/configuration.py` around lines 113 - 126, The current
branches in the method that check self.dumps and get_func_section(self.dumps,
func) always return False, which silently omits missing relinker targets even in
strict mode; instead, when espidf_missing_function_info is False
(strict/default), raise an exception (e.g., RuntimeError or a specific
ReLinkerError) with a clear message including self.library, self.name and func
so callers like library_c.append() will fail fast, and only print the warning
and return False when espidf_missing_function_info is True; update both branches
that currently return False to implement this raise-vs-print behavior
accordingly.
🧹 Nitpick comments (1)
test/test_integration.py (1)

137-139: The default strict relinker path still isn't exercised.

Every end-to-end call opts into missing_function_info=True, but builder/frameworks/espidf.py defaults custom_relinker_missing_function_info to "no". Please add one negative case with False and assert generator() / relink_c() raises on a missing symbol so the default integration behavior is covered.

Also applies to: 387-387, 457-457, 467-467

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_integration.py` around lines 137 - 139, Add a negative integration
test that sets missing_function_info=False (so it uses the framework default
custom_relinker_missing_function_info="no") and call the same flow that
currently uses missing_function_info=True, then assert that generator() or
relink_c() raises on the missing symbol; this will exercise the strict relinker
path. Repeat the same negative assertion for the other test locations that
currently pass True (the two other call sites mentioned) so the
default-integration behavior is covered for generator()/relink_c() when a symbol
is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@builder/frameworks/espidf.py`:
- Around line 2446-2452: The current flow skips repair when venv metadata
matches, allowing missing packages to persist; update the logic in the block
that checks venv_data_file/_is_venv_interpreter_valid/_is_venv_outdated so that
when the metadata matches (i.e., none of those conditions triggered) you still
call install_python_deps(venv_dir, deps) to reconcile/install any missing or
mismatched packages instead of exiting early; specifically, locate the
conditional around venv_data_file/venv_dir and after the final elif branch add a
call to install_python_deps(venv_dir, deps) (or replace the no-op path with that
call) while keeping existing calls to _recreate_and_save(...) when interpreter
is missing or venv is outdated.

In `@builder/relinker/configuration.py`:
- Around line 113-126: The current branches in the method that check self.dumps
and get_func_section(self.dumps, func) always return False, which silently omits
missing relinker targets even in strict mode; instead, when
espidf_missing_function_info is False (strict/default), raise an exception
(e.g., RuntimeError or a specific ReLinkerError) with a clear message including
self.library, self.name and func so callers like library_c.append() will fail
fast, and only print the warning and return False when
espidf_missing_function_info is True; update both branches that currently return
False to implement this raise-vs-print behavior accordingly.

---

Nitpick comments:
In `@test/test_integration.py`:
- Around line 137-139: Add a negative integration test that sets
missing_function_info=False (so it uses the framework default
custom_relinker_missing_function_info="no") and call the same flow that
currently uses missing_function_info=True, then assert that generator() or
relink_c() raises on the missing symbol; this will exercise the strict relinker
path. Repeat the same negative assertion for the other test locations that
currently pass True (the two other call sites mentioned) so the
default-integration behavior is covered for generator()/relink_c() when a symbol
is missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31fb1ca1-579d-4868-974c-b627dccef322

📥 Commits

Reviewing files that changed from the base of the PR and between 8812031 and 5f357b1.

📒 Files selected for processing (3)
  • builder/frameworks/espidf.py
  • builder/relinker/configuration.py
  • test/test_integration.py

@Jason2866 Jason2866 merged commit 0476594 into develop Mar 31, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 1, 2026
4 tasks
@Jason2866 Jason2866 deleted the relinker branch April 1, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant