From 5d4e96aeb898fa209e95340b404927b40c0701a1 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Fri, 7 Aug 2026 10:39:10 +0200 Subject: [PATCH] fix(ldgen): add the linker template to the generation fingerprint ldgen skips regeneration when a fingerprint over the section names, the linker fragment files, sdkconfig and Kconfig is unchanged. The linker script template was not part of that fingerprint, even though everything outside its mapping placeholders is copied into the generated script verbatim. The build system regenerates the preprocessed template whenever any of its inputs changes, not only when a linker fragment changes. When such a change reaches ldgen but leaves the section names alone, the fingerprint still matches, generation is skipped, and the image is linked against the previously generated script. The skip path updates the mtime of the output, so nothing in the build output shows that the change was dropped. Add the template to the fingerprint so that regenerating it always regenerates the linker script. Signed-off-by: Frantisek Hrbata --- tools/ldgen/ldgen.py | 13 +++++++++---- tools/test_build_system/test_rebuild.py | 26 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tools/ldgen/ldgen.py b/tools/ldgen/ldgen.py index 13e93d94af0..c53ef8779a2 100755 --- a/tools/ldgen/ldgen.py +++ b/tools/ldgen/ldgen.py @@ -41,7 +41,7 @@ class MutuallyExclusiveEatAllOption(MutuallyExclusiveOption, OptionEatAll): _RE_SECTION_NAME = re.compile(r'^\s*\d+\s+(\.\S+)', re.MULTILINE) -def _compute_fingerprint(sections_infos, fragment_files, config_file, kconfig_file): +def _compute_fingerprint(sections_infos, fragment_files, config_file, kconfig_file, input_file): """Compute a fingerprint from section names and mtimes of all inputs.""" hasher = hashlib.md5() @@ -50,9 +50,12 @@ def _compute_fingerprint(sections_infos, fragment_files, config_file, kconfig_fi names = _RE_SECTION_NAME.findall(info.content) hasher.update((archive + ':' + ','.join(names)).encode()) - # Mtimes of fragment files, sdkconfig, kconfig + # Mtimes of fragment files, the linker template, sdkconfig, kconfig. The + # template counts as an input because everything outside its mapping + # placeholders ends up in the generated script verbatim, so an edit to it + # changes the output even when no section name does. input_files = [p.name if hasattr(p, 'name') else p for p in fragment_files] - input_files += [p for p in (config_file, kconfig_file) if p] + input_files += [p.name if hasattr(p, 'name') else p for p in (input_file, config_file, kconfig_file) if p] for path in input_files: try: hasher.update(f'{path}:{os.path.getmtime(path)}'.encode()) @@ -190,7 +193,9 @@ def _run( # Check if we can skip generation entirely — section names and other # inputs unchanged since last run. fingerprint = ( - None if no_cache else _compute_fingerprint(sections_infos, fragment_files, config_file, kconfig_file) + None + if no_cache + else _compute_fingerprint(sections_infos, fragment_files, config_file, kconfig_file, input_file) ) if ( output_path diff --git a/tools/test_build_system/test_rebuild.py b/tools/test_build_system/test_rebuild.py index c6d4cf52904..3400d9664ae 100644 --- a/tools/test_build_system/test_rebuild.py +++ b/tools/test_build_system/test_rebuild.py @@ -175,6 +175,32 @@ def test_rebuild_ldgen_fingerprint(idf_py: IdfPyFunc, test_app_copy: Path) -> No assert skip_msg not in result.stdout, f'unexpected {skip_msg!r} in build output after fragment change' +@pytest.mark.usefixtures('idf_copy') +def test_rebuild_ldgen_template_change(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + """Verify that a change to the linker script template reaches the generated + script. ldgen copies everything outside the mapping placeholders from the + template into its output verbatim, so the template is an input in its own + right and must not be hidden behind the fingerprint skip. + """ + skip_msg = 'Skipping linker script generation, section names unchanged' + idf_path = Path(os.environ['IDF_PATH']) + template = idf_path / 'components/esp_system/ld/esp32/sections.ld.in' + generated = Path('build/esp-idf/esp_system/ld/sections.ld') + # An absolute symbol assignment: it is copied through to the generated + # script and does not depend on anything else in the build. + probe = '_ldgen_template_probe' + + logging.info('initial build') + idf_py('build') + + logging.info('adding a symbol to the template leaves section names and fragment files unchanged') + with open(template, 'a') as f: + f.write(f'\n{probe} = 0x3ff00abc;\n') + result = idf_py('build') + assert skip_msg not in result.stdout, f'unexpected {skip_msg!r} in build output after template change' + assert probe in generated.read_text(), 'template change did not reach the generated linker script' + + def test_rebuild_ldgen_lf_cache(idf_py: IdfPyFunc, test_app_copy: Path) -> None: """Verify the ldgen lf cache: when section names change but fragment files don't, parsed FragmentFile objects are loaded from cache rather than