olindgre 2ece01e1d7 Make powertrain-build not overlap with pybuild in site-packages
Change-Id: I7b59f3f04f0f787d35db0b9389f295bf1ad24f56
2024-09-17 10:25:04 +02:00

512 lines
19 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
# -*- coding: utf-8 -*-
"""Python module for abstracting Pybuild applications"""
from pathlib import Path
import json
from powertrain_build.interface.base import BaseApplication, Signal, MultipleProducersError, Domain, Interface
from powertrain_build.lib import logger
from powertrain_build.build_proj_config import BuildProjConfig
from powertrain_build.feature_configs import FeatureConfigs
from powertrain_build.unit_configs import UnitConfigs
from powertrain_build.user_defined_types import UserDefinedTypes
LOGGER = logger.create_logger("application")
def get_raster_to_raster_interfaces(rasters):
"""Generate a list of Interfaces for internal raster-to-raster signals.
Args:
rasters (list): Input rasters (from app.get_rasters())
Returns:
interfaces (list(interfaces)): List of unique raster-to-raster-interfaces.
"""
raster_pairs = []
for current_raster in rasters:
for corresponding_raster in [r for r in rasters if r != current_raster]:
# If we have interface a_b, no need to produce b_a.
if (corresponding_raster, current_raster) not in raster_pairs:
raster_pairs.append((current_raster, corresponding_raster))
return [Interface(raster[0], raster[1]) for raster in raster_pairs]
def get_internal_domain(rasters):
""" Create an internal domain of signals
Loops through all raster<->raster communications and adds them to a domain object
Args:
rasters (list(Raster)): rasters to calculate communication for
Returns:
domain (Domain): signals belonging to the same domain
"""
internal = Domain()
internal.set_name("internal")
for interface in get_raster_to_raster_interfaces(rasters):
internal.add_interface(interface)
return internal
def get_active_signals(signals, feature_cfg):
""" Filter out inactive signals. """
LOGGER.debug('Filtering %s', signals)
return [signal for signal in signals if feature_cfg.check_if_active_in_config(signal.properties['configs'])]
class Application(BaseApplication):
""" Object for holding information about a pybuild project """
def __init__(self):
self.name = None
self.pybuild = {'build_cfg': None,
'feature_cfg': None,
'unit_vars': {}}
self._insignals = None
self._outsignals = None
self._signals = None
self._raster_definitions = []
self._services = None
self._methods = []
self._enumerations = None
self._structs = None
def parse_definition(self, definition):
""" Parse ProjectCfg.json, get code switch values and read config.json files.
Add the information to the object.
Args:
definition (Path): Path to ProjectCfg.json
"""
self.pybuild['build_cfg'] = BuildProjConfig(str(definition))
self.name = self.pybuild['build_cfg'].name
self.pybuild['feature_cfg'] = FeatureConfigs(self.pybuild['build_cfg'])
unit_cfg = UnitConfigs(self.pybuild['build_cfg'], self.pybuild['feature_cfg'])
self.pybuild['unit_vars'] = unit_cfg.get_per_unit_cfg()
self.pybuild['user_defined_types'] = UserDefinedTypes(self.pybuild['build_cfg'], unit_cfg)
def get_domain_names(self):
""" Get domain names. """
return self.pybuild['build_cfg'].device_domains.values()
def get_domain_mapping(self):
""" Get device to signal domain mapping. """
return self.pybuild['build_cfg'].device_domains
def get_methods(self):
""" Get csp methods. """
if self._signals is None:
self._get_signals()
return self._methods
@property
def enumerations(self):
""" Get enumerations defined in the project. """
if self._enumerations is None:
self._enumerations = self.pybuild['user_defined_types'].get_enumerations()
return self._enumerations
@property
def structs(self):
""" Get structs defined in the project. """
if self._structs is None:
self._structs = self.pybuild['user_defined_types'].get_structs()
return self._structs
@property
def services(self):
""" Get interface to service mapping. """
if self._services is None:
services_file = self.get_services_file()
self._services = self.pybuild['build_cfg'].get_services(services_file)
return self._services
def get_service_mapping(self):
""" Get interface to service mapping. """
return self.services
def get_services_file(self):
""" Get path to file specifying interface to service mapping. """
return self.pybuild['build_cfg'].services_file
def get_name(self, definition):
""" Parse ProjectCfg.json and return the specified project name """
if self.name is None:
return BuildProjConfig(str(definition)).name
return self.name
def _get_signals(self):
""" Calculate parse all inport and outports of all models """
self._insignals = set()
self._outsignals = set()
defined_ports = {'inports': set(), 'outports': set()}
for unit, data in self.pybuild['unit_vars'].items():
self.parse_ports(data, defined_ports, self.pybuild['feature_cfg'], unit)
self.parse_csp_methods(data, self.pybuild['feature_cfg'], unit)
def parse_ports(self, port_data, defined_ports, feature_cfg, unit):
""" Parse ports for one model, based on code switch values.
Modifies the defined_ports dict and the object.
Args:
port_data (dict): port data for a model/unit
defined_ports (set): all known signals
feature_cfg (FeatureConfigs): pybuild parsed object for code switches
unit (string): Name of model/unit
"""
if self._signals is None:
self._signals = {}
for port_type, outport in {'outports': True, 'inports': False}.items():
for port_name, data in port_data.get(port_type, {}).items():
# Get what signals we are dealing with
if not feature_cfg.check_if_active_in_config(data['configs']):
continue
if port_name not in self._signals:
signal = Signal(port_name, self)
self._signals.update({port_name: signal})
else:
signal = self._signals[port_name]
# Add information about which models are involved while we are reading it
if outport:
try:
signal.set_producer(unit)
except MultipleProducersError as mpe:
LOGGER.debug(mpe.message)
signal.force_producer(unit)
self._outsignals.add(port_name)
else:
signal.consumers = unit
self._insignals.add(port_name)
defined_ports[port_type].add(port_name)
def parse_csp_methods(self, port_data, feature_cfg, unit):
""" Parse csp methods.
Args:
port_data (dict): port data for a model/unit.
feature_cfg (FeatureConfigs): pybuild parsed object for code switches
unit (string): Name of model/unit
"""
if self._signals is None:
self._signals = {}
methods = port_data.get('csp', {}).get('methods', {})
for method_name, data in methods.items():
if feature_cfg.check_if_active_in_config(data['configs']):
method = Method(self, unit)
method.parse_definition((method_name, data))
self._methods.append(method)
def get_signal_properties(self, signal):
""" Get properties for the signal from powertrain_build definition.
Args:
signal (Signal): Signal object
Returns:
properties (dict): Properties of the signal in pybuild
"""
# Hack: Take the first consumer or producer if any exists
for producer in signal.producer:
return self.pybuild['unit_vars'][producer]['outports'][signal.name]
for consumer in signal.consumers:
return self.pybuild['unit_vars'][consumer]['inports'][signal.name]
return {}
def get_rasters(self):
""" Get rasters parsed from powertrain_build.
Returns:
rasters (list): rasters parsed from powertrain_build
"""
if self._signals is None:
self._get_signals()
raster_definition = self.pybuild['build_cfg'].get_units_raster_cfg()
rasters = []
for raster_field, raster_content in raster_definition.items():
if raster_field in ['SampleTimes']:
continue
for name, content in raster_content.items():
if name in ['NoSched']:
continue
raster = Raster(self)
raster.parse_definition((name, content, self._signals))
rasters.append(raster)
return rasters
def get_models(self):
""" Get models and parse their config files.
Returns:
models (list(Model)): config.jsons parsed
"""
rasters = self.get_rasters()
# Since one model can exist in many rasters. Find all unique model names first.
cfg_dirs = self.pybuild['build_cfg'].get_unit_cfg_dirs()
model_names = set()
for raster in rasters:
model_names = model_names.union(raster.models)
models = []
for model_name in model_names:
if model_name not in cfg_dirs:
LOGGER.debug("%s is generated code. It does not have a config.", model_name)
continue
model = Model(self)
cfg_dir = cfg_dirs[model_name]
config = Path(cfg_dir, f'config_{model_name}.json')
model.parse_definition((model_name, config))
models.append(model)
return models
def get_translation_files(self):
""" Find all yaml files in translation file dirs.
Returns:
translation_files (list(Path)): translation files
"""
translation_files = []
cfg_dirs = self.pybuild['build_cfg'].get_translation_files_dirs()
for cfg_dir in cfg_dirs.values():
cfg_path = Path(cfg_dir)
translation_files.extend(cfg_path.glob('*.yaml'))
translation_files = list(set(translation_files))
return translation_files
class Raster(BaseApplication):
""" Object for holding information about a raster """
def __init__(self, app):
"""Construct a new Raster object.
Args:
app (powertrain_build.interface.application.Application): Pybuild project raster is part of
"""
self.app = app
self.name = str()
self._insignals = None
self._outsignals = None
self._available_signals = None
self.models = set()
def parse_definition(self, definition):
""" Parse the definition from powertrain_build.
Args:
definition (tuple):
name (string): Name of the raster
content (list): Models in the raster
app_signals (dict): All signals in all rasters
"""
self.name = definition[0]
self.models = set(definition[1])
self._available_signals = definition[2]
def _get_signals(self):
""" Add signals from the project to the raster if they are used here
Modifies the object itself.
"""
self._insignals = set()
self._outsignals = set()
self._signals = {}
if self._available_signals is None:
return
for signal in self._available_signals.values():
for consumer in signal.consumers:
if consumer in self.models:
self._signals.update({signal.name: signal})
self._insignals.add(signal.name)
if isinstance(signal.producer, set):
for producer in signal.producer:
if producer in self.models:
self._signals.update({signal.name: signal})
self._outsignals.add(signal.name)
else:
if signal.producer in self.models:
self._signals.update({signal.name: signal})
self._outsignals.add(signal.name)
def get_signal_properties(self, signal):
""" Get properties for the signal from powertrain_build definition.
Args:
signal (Signal): Signal object
Returns:
properties (dict): Properties of the signal in pybuild
"""
for producer in signal.producer:
if producer in self.app.pybuild['unit_vars']:
return self.app.get_signal_properties(signal)
return {}
class Model(BaseApplication):
""" Object for holding information about a model """
def __init__(self, app):
self.app = app
self.name = str()
self.config = None
self._insignals = None
self._outsignals = None
self._signal_specs = None
def get_signal_properties(self, signal):
""" Get properties for the signal from powertrain_build definition.
Args:
signal (Signal): Signal object
Returns:
properties (dict): Properties of the signal in pybuild
"""
if self._signal_specs is None:
self._get_signals()
if signal.name in self._signal_specs:
return self._signal_specs[signal.name]
return {}
def _get_signals(self):
""" Add signals from the project to the model if they are used here
Modifies the object itself.
Entrypoint for finding signals from the base class.
"""
self._insignals = set()
self._outsignals = set()
self._signals = {}
self._signal_specs = {}
self._parse_unit_config(self.config)
def _parse_unit_config(self, path):
""" Parse a unit config file.
Broken out of get_signals to be recursive for included configs.
"""
cfg = self._load_json(path)
for signal_spec in cfg['inports'].values():
signal = Signal(signal_spec['name'], self)
self._insignals.add(signal.name)
self._signals.update({signal.name: signal})
self._signal_specs[signal.name] = signal_spec
for signal_spec in cfg['outports'].values():
signal = Signal(signal_spec['name'], self)
self._outsignals.add(signal.name)
self._signals.update({signal.name: signal})
self._signal_specs[signal.name] = signal_spec
for include_cfg in cfg.get('includes', []):
LOGGER.debug('%s includes %s in %s', self.name, include_cfg, path.parent)
include_path = Path(path.parent, f'config_{include_cfg}.json')
self._parse_unit_config(include_path)
@staticmethod
def _load_json(path):
""" Small function that opens and loads a json file.
Exists to be mocked in unittests
"""
with open(path, encoding="utf-8") as fhndl:
return json.load(fhndl)
def parse_definition(self, definition):
""" Parse the definition from powertrain_build.
Args:
definition (tuple):
name (string): Name of the model
configuration (Path): Path to config file
"""
self.name = definition[0]
self.config = definition[1]
self._get_signals()
class Method(BaseApplication):
""" Object for holding information about a csp method call """
def __init__(self, app, unit):
"""Construct a new Method object.
Args:
app (powertrain_build.interface.application.Application): Pybuild project raster is part of.
unit (str): Model that the method is defined in.
"""
self.app = app
self.unit = unit
self.name = str()
self.namespace = str()
self.adapter = str()
self.description = None
self._signals = {}
self._insignals = set()
self._outsignals = set()
self._primitives = {}
self._properties = {}
def parse_definition(self, definition):
""" Parse the definition from powertrain_build.
Args:
definition (tuple):
name (string): Name of the model
configuration (dict): Configuration of method
"""
name = definition[0]
configuration = definition[1]
self.name = name
self.adapter = configuration['adapter']
self.namespace = configuration['namespace']
self._primitives[name] = configuration['primitive']
if 'description' in configuration:
self.description = configuration['description']
signals = configuration.get('ports', {})
outsignals = signals.get('out', {})
for signal_name, signal_data in outsignals.items():
signal = self._add_signal(signal_name)
signal.consumers = name
signal.set_producer(name)
self._primitives[signal_name] = signal_data['primitive']
self._properties[signal_name] = signal_data
self._outsignals.add(signal_name)
insignals = signals.get('in', {})
for signal_name, signal_data in insignals.items():
signal = self._add_signal(signal_name)
signal.consumers = name
signal.set_producer(name)
self._insignals.add(signal_name)
self._primitives[signal_name] = signal_data['primitive']
self._properties[signal_name] = signal_data
def _add_signal(self, signal_name):
""" Add a signal used by the method.
Args:
signal_name (str): Name of the signal
"""
if signal_name not in self._signals:
signal = Signal(signal_name, self)
self._signals.update({signal_name: signal})
else:
signal = self._signals[signal_name]
return signal
def get_signal_properties(self, signal):
""" Get properties for the signal from csp method configuration.
Args:
signal (Signal): Signal object
Returns:
properties (dict): Properties of the signal in pybuild
"""
return self._properties[signal.name]
def get_primitive(self, primitive_name):
""" Get primitive.
Args:
primitive_name (str): Name of primitive part
Returns:
primitive (str): Primitive
"""
return self._primitives[primitive_name]