| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0-or-later | |
| # Copyright (C) 2024, Google Inc. | |
| # | |
| # Author: Stefan Klug <stefan.klug@ideasonboard.com> | |
| # | |
| # This script looks for occurrences of the debug metadata controls in the source | |
| # tree and updates src/libcamera/control_ids_debug.yaml accordingly. It is meant | |
| # to be used during development to ease updating of the yaml file while | |
| # debugging. | |
| import argparse | |
| import logging | |
| import os | |
| import re | |
| import sys | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| logger = logging.getLogger(__name__) | |
| logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') | |
| try: | |
| import ruamel.yaml as ruyaml | |
| except: | |
| logger.error( | |
| f'Failed to import ruamel.yaml. Please install the ruamel.yaml package.') | |
| sys.exit(1) | |
| @dataclass | |
| class FoundMatch: | |
| file: os.PathLike | |
| whole_match: str | |
| line: int | |
| type: str | |
| name: str | |
| size: str = None | |
| def get_control_name(control): | |
| k = list(control.keys()) | |
| if len(k) != 1: | |
| raise Exception(f"Can't handle control entry with {len(k)} keys") | |
| return k[0] | |
| def find_debug_controls(dir): | |
| extensions = ['.cpp', '.h'] | |
| files = [p for p in dir.rglob('*') if p.suffix in extensions] | |
| # The following regex was tested on | |
| # set<Span<type>>( controls::debug::something , static_cast<type>(var) ) | |
| # set<>( controls::debug::something , static_cast<type>(var) ) | |
| # set( controls::debug::something , static_cast<type> (var) ) | |
| exp = re.compile(r'set' # set function | |
| r'(?:\<((?:[^)(])*)\>)?' # followed by a optional template param | |
| r'\(\s*controls::debug::(\w+)\s*,' # referencing a debug control | |
| ) | |
| matches = [] | |
| for p in files: | |
| with p.open('r') as f: | |
| for idx, line in enumerate(f): | |
| match = exp.search(line) | |
| if match: | |
| m = FoundMatch(file=p, line=idx, type=match.group(1), | |
| name=match.group(2), whole_match=match.group(0)) | |
| if m.type is not None and m.type.startswith('Span'): | |
| # Simple span type detection treating the last word | |
| # inside <> as type. | |
| r = re.match(r'Span<(?:.*\s+)(.*)>', m.type) | |
| m.type = r.group(1) | |
| m.size = '[n]' | |
| matches.append(m) | |
| return matches | |
| def main(argv): | |
| parser = argparse.ArgumentParser( | |
| description='Automatically updates control_ids_debug.yaml') | |
| parser.parse_args(argv[1:]) | |
| yaml = ruyaml.YAML() | |
| root_dir = Path(__file__).resolve().parent.parent | |
| ctrl_file = root_dir.joinpath('src/libcamera/control_ids_debug.yaml') | |
| matches = find_debug_controls(root_dir.joinpath('src')) | |
| doc = yaml.load(ctrl_file) | |
| controls = doc['controls'] | |
| # Create a map of names in the existing yaml for easier updating. | |
| controls_map = {} | |
| for control in controls: | |
| for k, v in control.items(): | |
| controls_map[k] = v | |
| obsolete_names = list(controls_map.keys()) | |
| for m in matches: | |
| if not m.type: | |
| p = m.file.relative_to(Path.cwd(), walk_up=True) | |
| logger.warning( | |
| f'{p}:{m.line + 1}: Failed to deduce type from {m.whole_match} ... skipping') | |
| continue | |
| p = m.file.relative_to(root_dir) | |
| desc = {'type': m.type, | |
| 'description': f'Debug control {m.name} found in {p}:{m.line}'} | |
| if m.size is not None: | |
| desc['size'] = m.size | |
| if m.name in controls_map: | |
| # Can't use == for modified check because of the special yaml dicts. | |
| update_needed = False | |
| if list(controls_map[m.name].keys()) != list(desc.keys()): | |
| update_needed = True | |
| else: | |
| for k, v in controls_map[m.name].items(): | |
| if v != desc[k]: | |
| update_needed = True | |
| break | |
| if update_needed: | |
| logger.info(f"Update control '{m.name}'") | |
| controls_map[m.name].clear() | |
| controls_map[m.name].update(desc) | |
| obsolete_names.remove(m.name) | |
| else: | |
| logger.info(f"Add control '{m.name}'") | |
| insert_before = len(controls) | |
| for idx, control in enumerate(controls): | |
| if get_control_name(control).lower() > m.name.lower(): | |
| insert_before = idx | |
| break | |
| controls.insert(insert_before, {m.name: desc}) | |
| # Remove elements from controls without recreating the list (to keep | |
| # comments etc.). | |
| idx = 0 | |
| while idx < len(controls): | |
| name = get_control_name(controls[idx]) | |
| if name in obsolete_names: | |
| logger.info(f"Remove control '{name}'") | |
| controls.pop(idx) | |
| else: | |
| idx += 1 | |
| with ctrl_file.open('w') as f: | |
| # Ruyaml looses the header. | |
| f.write(("# SPDX-License-Identifier: LGPL-2.1-or-later\n" | |
| "#\n" | |
| "# This file was generated by utils/gen-debug-controls.py\n" | |
| "#\n")) | |
| yaml.dump(doc, f) | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv)) |