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 1a3fa8983c5..953d222367d 100644 --- a/tools/test_build_system/test_rebuild.py +++ b/tools/test_build_system/test_rebuild.py @@ -183,6 +183,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