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 <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2026-08-07 10:39:10 +02:00
parent 6a9c44fe7e
commit da926220bd
2 changed files with 35 additions and 4 deletions

View File

@@ -27,7 +27,7 @@ from pyparsing import ParseFatalException
_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()
@@ -36,9 +36,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())
@@ -228,7 +231,9 @@ def main():
# 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

View File

@@ -174,6 +174,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