Paul Duffin | 50243ae | 2025-03-03 19:10:27 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Copyright (C) 2025 The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | |
| 17 | """Converts a set of aconfig protobuf flags to a Metalava config file.""" |
| 18 | |
| 19 | # Formatted using `pyformat -i scripts/aconfig-to-metalava-flags.py` |
| 20 | |
| 21 | import argparse |
| 22 | import sys |
| 23 | import xml.etree.ElementTree as ET |
| 24 | |
| 25 | from protos import aconfig_pb2 |
| 26 | |
| 27 | _READ_ONLY = aconfig_pb2.flag_permission.READ_ONLY |
| 28 | _ENABLED = aconfig_pb2.flag_state.ENABLED |
| 29 | _DISABLED = aconfig_pb2.flag_state.DISABLED |
| 30 | |
| 31 | # The namespace of the Metalava config file. |
| 32 | CONFIG_NS = 'http://www.google.com/tools/metalava/config' |
| 33 | |
| 34 | |
| 35 | def config_name(tag: str): |
| 36 | """Create a QName in the config namespace. |
| 37 | |
| 38 | :param:tag the name of the entity in the config namespace. |
| 39 | """ |
| 40 | return f'{{{CONFIG_NS}}}{tag}' |
| 41 | |
| 42 | |
| 43 | def main(): |
| 44 | """Program entry point.""" |
| 45 | args_parser = argparse.ArgumentParser( |
| 46 | description='Generate Metalava flags config from aconfig protobuf', |
| 47 | ) |
| 48 | args_parser.add_argument( |
| 49 | 'input', |
| 50 | help='The path to the aconfig protobuf file', |
| 51 | ) |
| 52 | args = args_parser.parse_args(sys.argv[1:]) |
| 53 | |
| 54 | # Read the parsed_flags from the protobuf file. |
| 55 | with open(args.input, 'rb') as f: |
| 56 | parsed_flags = aconfig_pb2.parsed_flags.FromString(f.read()) |
| 57 | |
| 58 | # Create the structure of the XML config file. |
| 59 | config = ET.Element(config_name('config')) |
| 60 | api_flags = ET.SubElement(config, config_name('api-flags')) |
| 61 | # Create an <api-flag> element for each parsed_flag. |
| 62 | for flag in parsed_flags.parsed_flag: |
| 63 | if flag.permission == _READ_ONLY: |
| 64 | # Ignore any read only disabled flags as Metalava assumes that as the |
| 65 | # default when an <api-flags/> element is provided so this reduces the |
| 66 | # size of the file. |
| 67 | if flag.state == _DISABLED: |
| 68 | continue |
| 69 | mutability = 'immutable' |
| 70 | else: |
| 71 | mutability = 'mutable' |
| 72 | if flag.state == _ENABLED: |
| 73 | status = 'enabled' |
| 74 | else: |
| 75 | status = 'disabled' |
| 76 | attributes = { |
| 77 | 'package': flag.package, |
| 78 | 'name': flag.name, |
| 79 | 'mutability': mutability, |
| 80 | 'status': status, |
| 81 | } |
| 82 | # Convert the attribute names into qualified names in, what will become, the |
| 83 | # default namespace for the XML file. This is needed to ensure that the |
| 84 | # attribute will be written in the XML file without a prefix, e.g. |
| 85 | # `name="flag_name"`. Without it, a namespace prefix, e.g. `ns1`, will be |
| 86 | # synthesized for the attribute when writing the XML file, i.e. it |
| 87 | # will be written as `ns1:name="flag_name"`. Strictly speaking, that is |
| 88 | # unnecessary as the "Namespaces in XML 1.0 (Third Edition)" specification |
| 89 | # says that unprefixed attribute names have no namespace. |
| 90 | qualified_attributes = {config_name(k): v for (k, v) in attributes.items()} |
| 91 | ET.SubElement(api_flags, config_name('api-flag'), qualified_attributes) |
| 92 | |
| 93 | # Create a tree and add white space so it will pretty print when written out. |
| 94 | tree = ET.ElementTree(config) |
| 95 | ET.indent(tree) |
| 96 | |
| 97 | # Write the tree using the config namespace as the default. |
| 98 | tree.write(sys.stdout, encoding='unicode', default_namespace=CONFIG_NS) |
| 99 | sys.stdout.close() |
| 100 | |
| 101 | |
| 102 | if __name__ == '__main__': |
| 103 | main() |