| # Copyright 2019 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Generate build API caller scripts. |
| |
| Creates executable scripts of the form service__method for each Build API |
| endpoint. The scripts can be called without arguments. They instead make |
| assumptions about the input and output json file names and locations. |
| |
| The system supports checking in example files which are automatically copied |
| in to the input file location. The example files are found in the |
| call_templates/ directory, and of the form "service__method_input.json". |
| When not found, it will write out an empty json dict to the input file. |
| |
| See the api/contrib and api/ READMEs for more info about the gen_call_scripts |
| script and the Build API itself, respectively. |
| https://chromium.googlesource.com/chromiumos/chromite/+/HEAD/api/contrib/README.md |
| https://chromium.googlesource.com/chromiumos/chromite/+/HEAD/api/README.md |
| """ |
| |
| import logging |
| import os |
| import re |
| |
| from chromite.api import message_util |
| from chromite.api import router as router_lib |
| from chromite.api.gen.chromite.api import build_api_config_pb2 |
| from chromite.lib import build_target_lib |
| from chromite.lib import commandline |
| from chromite.lib import constants |
| from chromite.lib import osutils |
| |
| |
| EXAMPLES_PATH = os.path.join(os.path.dirname(__file__), "call_templates") |
| OUTPUT_PATH = os.path.join(os.path.dirname(__file__), "call_scripts") |
| _SCRIPT_TEMPLATE_FILE = os.path.join(EXAMPLES_PATH, "script_template") |
| SCRIPT_TEMPLATE = osutils.ReadFile(_SCRIPT_TEMPLATE_FILE) |
| BUILD_TARGET_FILE = os.path.join(OUTPUT_PATH, ".build_target") |
| CONFIG_FILE = os.path.join(OUTPUT_PATH, "config.json") |
| |
| |
| def _camel_to_snake(string): |
| # Transform FooBARBaz into FooBAR_Baz. Avoids making Foo_B_A_R_Baz. |
| sub1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", string) |
| # Transform FooBAR_Baz into Foo_BAR_Baz. |
| sub2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub1) |
| # Lower case to get foo_bar_baz. |
| return sub2.lower() |
| |
| |
| def _get_services(): |
| """Get all service: [method, ...] mappings. |
| |
| Parses chromite.api.FooService/Method to foo: [method, ...]. |
| |
| Returns: |
| dict |
| """ |
| router = router_lib.GetRouter() |
| return router.ListMethods() |
| |
| |
| def get_services(): |
| services = {} |
| for service_method in _get_services(): |
| service, method = service_method.split("/") |
| service_parts = service.split(".") |
| full_service_name = service_parts[-1] |
| service_name = full_service_name.replace("Service", "") |
| |
| final_service = _camel_to_snake(service_name) |
| final_method = _camel_to_snake(method) |
| |
| if not final_service in services: |
| services[final_service] = { |
| "name": final_service, |
| "full_name": full_service_name, |
| "methods": [], |
| } |
| |
| services[final_service]["methods"].append( |
| { |
| "name": final_method, |
| "full_name": method, |
| } |
| ) |
| |
| return list(services.values()) |
| |
| |
| def write_script(filename, service, method) -> None: |
| contents = SCRIPT_TEMPLATE % {"SERVICE": service, "METHOD": method} |
| script_path = os.path.join(OUTPUT_PATH, filename) |
| osutils.WriteFile(script_path, contents, makedirs=True) |
| os.chmod(script_path, 0o755) |
| |
| |
| def write_scripts(build_target, force=False) -> None: |
| fmt_vars = { |
| "build_target": build_target, |
| "chroot": constants.DEFAULT_CHROOT_PATH, |
| "src_root": constants.SOURCE_ROOT, |
| } |
| for service_data in get_services(): |
| for method_data in service_data["methods"]: |
| filename = "__".join([service_data["name"], method_data["name"]]) |
| logging.info("Writing %s", filename) |
| write_script( |
| filename, service_data["full_name"], method_data["full_name"] |
| ) |
| |
| example_input = os.path.join( |
| EXAMPLES_PATH, "%s_input.json" % filename |
| ) |
| input_file = os.path.join(OUTPUT_PATH, "%s_input.json" % filename) |
| |
| if not force and not _input_file_empty(input_file): |
| logging.info("%s exists, skipping.", input_file) |
| elif os.path.exists(example_input): |
| logging.info( |
| "Example %s exists, building input.", example_input |
| ) |
| content = osutils.ReadFile(example_input) |
| osutils.WriteFile(input_file, content % fmt_vars) |
| elif not os.path.exists(input_file): |
| logging.info("No input could be found, writing empty input.") |
| osutils.WriteFile(input_file, "{}") |
| |
| |
| def _input_file_empty(input_file): |
| if not os.path.exists(input_file): |
| return True |
| contents = osutils.ReadFile(input_file).strip() |
| return not contents or contents == "{}" |
| |
| |
| def write_config(call_type) -> None: |
| config = build_api_config_pb2.BuildApiConfig() |
| config.call_type = call_type |
| msg_handler = message_util.get_message_handler( |
| CONFIG_FILE, message_util.FORMAT_JSON |
| ) |
| msg_handler.write_from(config) |
| |
| |
| def read_build_target_file(): |
| if os.path.exists(BUILD_TARGET_FILE): |
| return osutils.ReadFile(BUILD_TARGET_FILE).strip() |
| else: |
| return None |
| |
| |
| def write_build_target_file(build_target_name) -> None: |
| osutils.WriteFile(BUILD_TARGET_FILE, build_target_name) |
| |
| |
| def GetParser(): |
| """Build the argument parser.""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--force", |
| action="store_true", |
| default=False, |
| help="Force replace all input files, even if not empty.", |
| ) |
| parser.add_argument( |
| "-b", |
| "--build-target", |
| dest="build_target_name", |
| default="amd64-generic", |
| help="Generate the configs with the given build target. Implies " |
| "--force when generating for a new build target. Defaults to " |
| "amd64-generic.", |
| ) |
| |
| call_type_group = parser.add_mutually_exclusive_group() |
| call_type_group.add_argument( |
| "--validate-only", |
| action="store_const", |
| dest="call_type", |
| const=build_api_config_pb2.CALL_TYPE_VALIDATE_ONLY, |
| help="Generate a validate-only config.", |
| ) |
| call_type_group.add_argument( |
| "--mock-success", |
| action="store_const", |
| dest="call_type", |
| const=build_api_config_pb2.CALL_TYPE_MOCK_SUCCESS, |
| help="Generate a mock-success config.", |
| ) |
| call_type_group.add_argument( |
| "--mock-failure", |
| action="store_const", |
| dest="call_type", |
| const=build_api_config_pb2.CALL_TYPE_MOCK_FAILURE, |
| help="Generate a mock-success config.", |
| ) |
| call_type_group.add_argument( |
| "--mock-invalid", |
| action="store_const", |
| dest="call_type", |
| const=build_api_config_pb2.CALL_TYPE_MOCK_INVALID, |
| help="Generate a mock-success config.", |
| ) |
| |
| return parser |
| |
| |
| def _ParseArgs(argv): |
| """Parse and validate arguments.""" |
| parser = GetParser() |
| opts = parser.parse_args(argv) |
| |
| opts.build_target = build_target_lib.BuildTarget(opts.build_target_name) |
| opts.force = ( |
| opts.force or opts.build_target.name != read_build_target_file() |
| ) |
| |
| opts.Freeze() |
| return opts |
| |
| |
| def main(argv) -> None: |
| opts = _ParseArgs(argv) |
| write_scripts(opts.build_target.name, force=opts.force) |
| write_build_target_file(opts.build_target.name) |
| write_config(opts.call_type or build_api_config_pb2.CALL_TYPE_EXECUTE) |