blob: da3ed4c826275d2defa92120eb4ec63a14995d7d [file] [log] [blame] [edit]
# 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)