#!/usr/bin/env python3 # Copyright (C) 2025 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Converts a set of aconfig protobuf flags to a Metalava config file.""" # Formatted using `pyformat -i scripts/aconfig-to-metalava-flags.py` import argparse import sys import xml.etree.ElementTree as ET from protos import aconfig_pb2 _READ_ONLY = aconfig_pb2.flag_permission.READ_ONLY _ENABLED = aconfig_pb2.flag_state.ENABLED _DISABLED = aconfig_pb2.flag_state.DISABLED # The namespace of the Metalava config file. CONFIG_NS = 'http://www.google.com/tools/metalava/config' def config_name(tag: str): """Create a QName in the config namespace. :param:tag the name of the entity in the config namespace. """ return f'{{{CONFIG_NS}}}{tag}' def main(): """Program entry point.""" args_parser = argparse.ArgumentParser( description='Generate Metalava flags config from aconfig protobuf', ) args_parser.add_argument( 'input', help='The path to the aconfig protobuf file', ) args = args_parser.parse_args(sys.argv[1:]) # Read the parsed_flags from the protobuf file. with open(args.input, 'rb') as f: parsed_flags = aconfig_pb2.parsed_flags.FromString(f.read()) # Create the structure of the XML config file. config = ET.Element(config_name('config')) api_flags = ET.SubElement(config, config_name('api-flags')) # Create an element for each parsed_flag. for flag in parsed_flags.parsed_flag: if flag.permission == _READ_ONLY: # Ignore any read only disabled flags as Metalava assumes that as the # default when an element is provided so this reduces the # size of the file. if flag.state == _DISABLED: continue mutability = 'immutable' else: mutability = 'mutable' if flag.state == _ENABLED: status = 'enabled' else: status = 'disabled' attributes = { 'package': flag.package, 'name': flag.name, 'mutability': mutability, 'status': status, } # Convert the attribute names into qualified names in, what will become, the # default namespace for the XML file. This is needed to ensure that the # attribute will be written in the XML file without a prefix, e.g. # `name="flag_name"`. Without it, a namespace prefix, e.g. `ns1`, will be # synthesized for the attribute when writing the XML file, i.e. it # will be written as `ns1:name="flag_name"`. Strictly speaking, that is # unnecessary as the "Namespaces in XML 1.0 (Third Edition)" specification # says that unprefixed attribute names have no namespace. qualified_attributes = {config_name(k): v for (k, v) in attributes.items()} ET.SubElement(api_flags, config_name('api-flag'), qualified_attributes) # Create a tree and add white space so it will pretty print when written out. tree = ET.ElementTree(config) ET.indent(tree) # Write the tree using the config namespace as the default. tree.write(sys.stdout, encoding='unicode', default_namespace=CONFIG_NS) sys.stdout.close() if __name__ == '__main__': main()