| #!/usr/bin/env python3 |
| |
| # Utilities for manipulating/testing the MAINTAINERS.yml data. |
| |
| # Copyright (C) 2026 Free Software Foundation, Inc. |
| # |
| # This file is part of GCC. |
| # |
| # GCC is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 3, or (at your option) |
| # any later version. |
| # |
| # GCC is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with GCC; see the file COPYING. If not, write to |
| # the Free Software Foundation, 51 Franklin Street, Fifth Floor, |
| # Boston, MA 02110-1301, USA. |
| |
| import json |
| import jsonschema |
| import sys |
| import unidecode |
| import yaml |
| |
| # Prefer the libYAML implementations if available. Fall back to the |
| # pure python versions as a backup. |
| try: |
| from yaml import CLoader as Loader, CDumper as Dumper |
| except: |
| from yaml import Loader, Dumper |
| |
| label_pattern = '[a-zA-Z][-+a-zA-Z0-9]*(/[a-zA-Z][-+a-zA-Z0-9]*)+' |
| |
| maintainer_schema = { |
| '$schema': 'https://json-schema.org/draft/2020-12/schema', |
| 'type': 'object', |
| 'properties': { |
| 'users': { |
| 'type': 'array', |
| 'items': { |
| 'type': 'object', |
| 'properties': { |
| 'sn': { |
| 'type': 'string', |
| 'minlength': 1, |
| }, |
| 'cn': { |
| 'type': 'string', |
| 'minlength': 1, |
| }, |
| 'email': { |
| 'type': 'array', |
| 'items': { |
| 'type': 'string', |
| 'format': 'email', |
| }, |
| }, |
| 'roles': { |
| 'type': 'array', |
| 'items': { |
| 'oneOf': [ |
| { |
| 'type': 'string', |
| 'enum': ['Global', 'WriteAfter', 'BZ'], |
| }, |
| { |
| 'type': 'object', |
| 'properties': { |
| 'WriteAfter': { |
| 'type': 'string', |
| 'format': 'email', |
| }, |
| 'DCO': { |
| 'type': 'string', |
| 'format': 'email', |
| }, |
| }, |
| 'additionalProperties': False, |
| }, |
| { |
| 'type': 'object', |
| 'properties': { |
| 'Maintainer': { |
| 'type': 'string', |
| }, |
| 'Reviewer': { |
| 'type': 'string', |
| }, |
| 'email': { |
| 'type': 'string', |
| 'format': 'email', |
| }, |
| }, |
| 'additionalProperties': False, |
| 'oneOf': [ |
| {'required': ["Maintainer"]}, |
| {'required': ["Reviewer"]}, |
| ], |
| }, |
| ], |
| }, |
| 'minlength': 1, |
| }, |
| 'account': { |
| 'type': 'string', |
| 'minlength': 1, |
| 'pattern': '[a-zA-Z][a-zA-Z0-9]*', |
| }, |
| 'aliases': { |
| 'type': 'array', |
| 'minlength': 1, |
| 'items': { |
| 'type': 'string', |
| 'minlength': 1, |
| }, |
| }, |
| 'inactive': { |
| 'type': 'boolean', |
| }, |
| }, |
| 'additionalProperties': False, |
| 'required': ['sn', 'cn', 'email', 'roles'], |
| }, |
| }, |
| 'subsystems': { |
| 'type': 'array', |
| 'items': { |
| 'type': 'object', |
| 'properties': { |
| 'name': { |
| 'type': 'string', |
| 'minlength': 1, |
| }, |
| 'class': { |
| 'type': 'string', |
| 'minlength': 1, |
| }, |
| 'labels': { |
| 'type': 'array', |
| # Disabled until we populate the labels |
| # 'minlength': 1, |
| 'items': { |
| 'type': 'string', |
| 'minlength': 3, |
| 'pattern': label_pattern, |
| }, |
| }, |
| }, |
| 'additionalProperties': False, |
| 'required': ['name', 'labels'], |
| }, |
| }, |
| }, |
| } |
| |
| error_count = 0 |
| warning_count = 0 |
| |
| |
| def _fatal(msg): |
| print(f"fatal: {msg}", file=sys.stderr) |
| sys.exit(1) |
| |
| |
| def _error(msg): |
| global error_count |
| print(f"error: {msg}", file=sys.stderr) |
| error_count += 1 |
| |
| |
| def _warning(msg): |
| global warning_count |
| print(f"warning: {msg}", file=sys.stderr) |
| warning_count += 1 |
| |
| |
| def _format_path(path, data): |
| """Try to turn a validation error path into something more readable""" |
| elts = [""] |
| cur = data |
| try: |
| for elt in path: |
| if isinstance(cur, list) and isinstance(elt, int): |
| item = cur[elt] |
| if isinstance(item, dict) and 'cn' in item: |
| elts[-1] += f"[cn = {item['cn']}]" |
| elif isinstance(item, dict) and 'name' in item: |
| elts[-1] += f"[name = {item['name']}]" |
| else: |
| elts[-1] += f"[(anon index): {elt}]" |
| cur = item |
| else: |
| cur = cur[elt] |
| elts.append(str(elt)) |
| except Exception as e: |
| print('/'.join(elts)) |
| raise e |
| if elts[0] == "": |
| elts = elts[1:] |
| return '/'.join(elts) |
| |
| |
| def _check_schema(data): |
| try: |
| schema = jsonschema.validators.Draft202012Validator(maintainer_schema) |
| errors = sorted(schema.iter_errors(data), key=lambda e: e.path) |
| for err in errors: |
| _error(f"{_format_path(err.path, data)}: {err.message}") |
| except jsonschema.exceptions.SchemaError as e: |
| _fatal(str(e)) |
| return |
| |
| |
| def validate(data): |
| """Check the data against the schema and our own consistency checks""" |
| _check_schema(data) |
| # The data conforms to the expected schema, so now run some data |
| # validation checks. |
| |
| # Maintainer and Reviewer roles need to match entries in the |
| # subsystems list; Maintainer entires must also have a class entry, though |
| # that is optional for Reviewers. |
| for u in data['users']: |
| # Users with the 'BZ' role should not have any other roles; we |
| # can quickly skip the additional checks if that is the case. |
| if len(u['roles']) == 1 and u['roles'][0] == 'BZ': |
| continue |
| seen_writeafter = False |
| only_dco = True |
| for r in u['roles']: |
| if isinstance(r, str): |
| if r == 'BZ': |
| _error( |
| f"User '{u['cn']}' has 'BZ' role as well as others." |
| ) |
| if r == 'WriteAfter': |
| seen_writeafter = True |
| only_dco = False |
| continue |
| if 'WriteAfter' in r: |
| seen_writeafter = True |
| need_class = True |
| if 'DCO' in r: |
| continue |
| only_dco = False |
| n = r.get('Maintainer') |
| if not n: |
| need_class = False |
| n = r.get('Reviewer') |
| if n: |
| subsystems = list( |
| filter(lambda s: s['name'] == n, data['subsystems']) |
| ) |
| if len(subsystems) == 1: |
| if need_class and not 'class' in subsystems[0]: |
| _error(f"subsystem '{n}' missing a class.") |
| elif len(subsystems) == 0: |
| _error(f"No subsystem entry for '{n}'.") |
| else: |
| _error(f"Multiple subsystem entries for '{n}'.") |
| if not seen_writeafter and not only_dco: |
| _error(f"User '{u['cn']}' lacks WriteAfter role.") |
| if error_count: |
| sys.exit(1) |
| return |
| |
| |
| def load(file): |
| with open(file, "r", encoding="utf-8") as f: |
| data = yaml.load(f, Loader=Loader) |
| return data |
| |
| |
| def main(): |
| if len(sys.argv) != 2: |
| print(f"Usage: {sys.argv[0]} path-to-MAINTAINERS.yml") |
| return 1 |
| validate(load(sys.argv[1])) |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |