Axel Andersson 816553f1bc Added powertrain-build entrypoint
The entrypoint will make it easier to install powertrain-build to an
isolated venv without having to call python -m powertrain_build.

Change-Id: I3850c97d17707f9bc03640bd1d997508637d97ba
2024-10-30 10:33:42 +01:00

181 lines
6.2 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
"""Module that handles the update of call sources."""
import argparse
import re
import sys
from typing import List, Optional
from ruamel.yaml import YAML
from pathlib import Path
PARSER_HELP = "Update call sources for method calls in source files."
def configure_parser(parser: argparse.ArgumentParser):
"""Configure the parser for the update call sources command."""
parser.add_argument("interface", help="Interface specification dict", type=Path)
parser.add_argument("src_dir", help="Path to source file directory", type=Path)
parser.add_argument(
"-p",
"--project-config",
type=Path,
default=None,
help="Path to project config json file",
)
parser.set_defaults(func=update_call_sources_cli)
def update_call_sources_cli(args: argparse.Namespace):
"""CLI function for updating call sources."""
method_config = read_project_config(args.project_config)
with open(args.interface, encoding="utf-8") as interface_file:
yaml = YAML(typ='safe', pure=True)
adapter_spec = yaml.load(interface_file)
update_call_sources(args.src_dir, adapter_spec, method_config)
def main(argv: Optional[List[str]] = None):
""" Main function for stand alone execution."""
parser = argparse.ArgumentParser(description=PARSER_HELP)
configure_parser(parser)
args = parser.parse_args(argv)
args.func(args)
def read_project_config(project_config_path):
""" Reads project config file and extract method specific settings if they are present.
Args:
project_config_path (Path): path to the ProjectCfg.json file
Returns:
method_config (dict): dictionary of method related configs.
"""
project_info = {}
if project_config_path is not None:
with project_config_path.open() as config_file:
yaml = YAML(typ='safe', pure=True)
config = yaml.load(config_file)
project_info = config["ProjectInfo"]
method_config = {
"adapter_declarations_change": project_info.get("adapterDeclarations", None)
}
method_config["method_call_wrapper"] = {
"pre": project_info.get("methodPreCallWrapper", ""),
"post": project_info.get("methodPostCallWrapper", "")
}
for key, value in method_config["method_call_wrapper"].items():
if value != "":
method_config["method_call_wrapper"][key] = value + "\n"
return method_config
def is_method_adapter(adapter):
""" Check if adapter has methods in it
Args:
adapter (dict): adapter specification
Returns:
methods_in_adapter (bool): true if adapter contains methods,
false otherwise
"""
methods_in_adapter = "methods" in adapter and len(adapter["methods"]) > 0
return methods_in_adapter
def update_call_sources(src_dir, adapter_spec, method_config):
""" Update the source files for specified method calls with
adapter function calls that trigger the methods.
Args:
src_dir (Path): path to folder for method call sources
adapter_spec (list): adapter specifications with methods
method_config (dict): project specific method configs
"""
method_adapters = [a for a in adapter_spec if is_method_adapter(a)]
for adapter in method_adapters:
for method in adapter["methods"]:
method_src = src_dir / (method["name"] + ".c")
with method_src.open("r+") as src_file:
old_src = src_file.read()
new_src = generate_src_code(adapter, method, old_src, method_config)
method_src.unlink()
with open(method_src.with_suffix(".cpp"), "w", encoding="utf-8") as dst_file:
dst_file.write(new_src)
method_header = src_dir / (method["name"] + ".h")
with method_header.open("r+") as header_file:
old_header = header_file.read()
new_header = generate_header_code(method_config, old_header)
header_file.seek(0)
header_file.write(new_header)
header_file.truncate()
def generate_header_code(method_config, old_header):
""" Change header code to include project specific adapter wrapper.
Args:
method_config (dict): project specific method settings
old_header (string): header source code
Returns:
new_header (string): modified header source code
"""
adapter_pattern = r'(?<=#include ")adapter_wrapper.hh'
if method_config["adapter_declarations_change"]:
new_header = re.sub(
adapter_pattern, method_config["adapter_declarations_change"], old_header
)
return new_header
return old_header
def generate_src_code(adapter, method, old_src, method_config):
""" Generate the method call source code to trigger the method call
Args:
adapter (dict): adapter specification
method (dict): method specification
old_src (string): method call source code with mock call
method_config (dict): project specific method settings
Returns:
new_src (string): method call source code with adapter function call
that triggers method
"""
call_pattern = (
r" \/\* CSP method call\n"
r"(.*)ADAPTER->METHOD(.*)"
r" CSP method call end\*\/\n"
)
dummy_pattern = (
r" \/\* C dummy call\*\/" r".*" r" \/\* C dummy call end\*\/\n"
)
comment = (
r"\/\* Used for running spm without csp, such as silver\n"
r" This code should be replaced when using csp. \*\/\n"
)
name = method["name"]
adapter_name = [adapter["name"].title()]
namespace = []
if method["namespace"] != "":
namespace = [method["namespace"]]
adapter = "::".join(namespace + adapter_name) + "->" + name
pre_call = method_config["method_call_wrapper"]["pre"]
post_call = method_config["method_call_wrapper"]["post"]
sans_comment = re.sub(comment, "", old_src)
sans_dummy_call = re.sub(dummy_pattern, "", sans_comment, flags=re.DOTALL)
new_src = re.sub(
call_pattern, pre_call + r"\1" + adapter + r"\2" + post_call, sans_dummy_call, flags=re.DOTALL
)
return new_src
if __name__ == "__main__":
main(sys.argv[1:])