powertrain-build/pybuild/build_proj_config.py
Henrik Wahlqvist e4085f78e7 Add new includes config for tree structure include
There are cases when we have header files in a folder structure,
so we should not flatten the included paths.
So this commit adds get_includes_paths_tree, that should be self explaining,
and renames get_includes_paths to get_includes_paths_flat.

Change-Id: I2cff35687adb2a6caee1f4d3c631e62b8c5b53e4
2024-07-03 16:55:04 +02:00

612 lines
21 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
# -*- coding: utf-8 -*-
"""Module used to read project and base configuration files and provides methods for abstraction."""
import glob
import json
import os
import shutil
import pathlib
from pprint import pformat
from pybuild.lib.helper_functions import deep_dict_update
from pybuild.versioncheck import Version
class BuildProjConfig:
"""A class holding build project configurations."""
def __init__(self, prj_config_file):
"""Read project configuration file to internal an representation.
Args:
prj_config_file (str): Project config filename
"""
super().__init__()
self._prj_cfg_file = prj_config_file
prj_root_dir, _ = os.path.split(prj_config_file)
self._prj_root_dir = os.path.abspath(prj_root_dir)
with open(prj_config_file, 'r', encoding="utf-8") as pcfg:
self._prj_cfg = json.load(pcfg)
if not Version.is_compatible(self._prj_cfg.get('ConfigFileVersion')):
raise ValueError('Incompatible project config file version.')
# Load a basic config that can be common for several projects
# the local config overrides the base config
if 'BaseConfig' in self._prj_cfg:
fil_tmp = os.path.join(prj_root_dir, self._prj_cfg['BaseConfig'])
fil_ = os.path.abspath(fil_tmp)
with open(fil_, 'r', encoding="utf-8") as bcfg:
base_cnfg = json.load(bcfg)
deep_dict_update(self._prj_cfg, base_cnfg)
if not Version.is_compatible(self._prj_cfg.get('BaseConfigFileVersion')):
raise ValueError('Incompatible base config file version.')
self.has_yaml_interface = self._prj_cfg['ProjectInfo'].get('yamlInterface', False)
self.device_domains = self._get_device_domains()
self.services_file = self._get_services_file()
self._load_unit_configs()
self._add_global_const_file()
self._all_units = []
self._calc_all_units()
self.name = self._prj_cfg['ProjectInfo']['projConfig']
self.allow_undefined_unused = self._prj_cfg['ProjectInfo'].get('allowUndefinedUnused', True)
self._scheduler_prefix = self._prj_cfg['ProjectInfo'].get('schedulerPrefix', '')
if self._scheduler_prefix:
self._scheduler_prefix = self._scheduler_prefix + '_'
def __repr__(self):
"""Get string representation of object."""
return pformat(self._prj_cfg['ProjectInfo'])
def _get_device_domains(self):
file_name = self._prj_cfg['ProjectInfo'].get('deviceDomains')
full_path = pathlib.Path(self._prj_root_dir, file_name)
if full_path.is_file():
with open(full_path, 'r', encoding="utf-8") as device_domains:
return json.loads(device_domains.read())
return {}
def _get_services_file(self):
file_name = self._prj_cfg['ProjectInfo'].get('serviceInterfaces', '')
full_path = pathlib.Path(self._prj_root_dir, file_name)
return full_path
@staticmethod
def get_services(services_file):
"""Get the services from the services file.
Args:
services_file (pathlib.Path): The services file.
Returns:
(dict): The services.
"""
if services_file.is_file():
with services_file.open() as services:
return json.loads(services.read())
return {}
def _load_unit_configs(self):
"""Load Unit config json file.
This file contains which units are included in which projects.
"""
if 'UnitCfgs' in self._prj_cfg:
fil_tmp = os.path.join(self._prj_root_dir, self._prj_cfg['UnitCfgs'])
with open(fil_tmp, 'r', encoding="utf-8") as fpr:
tmp_unit_cfg = json.load(fpr)
sample_times = tmp_unit_cfg.pop('SampleTimes')
self._unit_cfg = {
'Rasters': tmp_unit_cfg,
'SampleTimes': sample_times
}
else:
raise ValueError('UnitCfgs is not specified in project config')
def _add_global_const_file(self):
"""Add the global constants definition to the 'not_scheduled' time raster."""
ugc = self.get_use_global_const()
if ugc:
self._unit_cfg['Rasters'].setdefault('NoSched', []).append(ugc)
def create_build_dirs(self):
"""Create the necessary output build dirs if they are missing.
Clear the output build dirs if they exist.
"""
src_outp = self.get_src_code_dst_dir()
if os.path.exists(src_outp):
shutil.rmtree(src_outp)
os.makedirs(src_outp)
log_outp = self.get_log_dst_dir()
if os.path.exists(log_outp):
shutil.rmtree(log_outp)
os.makedirs(log_outp)
rep_outp = self.get_reports_dst_dir()
if os.path.exists(rep_outp):
shutil.rmtree(rep_outp)
os.makedirs(rep_outp)
unit_cfg_outp = self.get_unit_cfg_deliv_dir()
if os.path.exists(unit_cfg_outp):
shutil.rmtree(unit_cfg_outp)
if unit_cfg_outp is not None:
os.makedirs(unit_cfg_outp)
def get_a2l_cfg(self):
""" Get A2L configuration from A2lConfig.
Returns:
config (dict): A2L configuration
"""
a2l_config = self._prj_cfg.get('A2lConfig', {})
return {
'name': a2l_config.get('name', self._prj_cfg["ProjectInfo"]["projConfig"]),
'allow_kp_blob': a2l_config.get('allowKpBlob', True),
'ip_address': a2l_config.get('ipAddress', "169.254.4.10"),
'ip_port': '0x%X' % a2l_config.get('ipPort', 30000),
'asap2_version': a2l_config.get('asap2Version', "1 51")
}
def get_unit_cfg_deliv_dir(self):
"""Get the directory where to put the unit configuration files.
If this key is undefined, or set to None, the unit-configs will
not be copied to the output folder.
Returns:
A path to the unit deliver dir, or None
"""
if 'unitCfgDeliveryDir' in self._prj_cfg['ProjectInfo']:
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['unitCfgDeliveryDir']))
return None
def get_root_dir(self):
"""Get the root directory of the project.
Returns:
A path to the project root (with wildcards)
"""
return self._prj_root_dir
def get_src_code_dst_dir(self):
"""Return the absolute path to the source output folder."""
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['srcCodeDstDir']))
def get_composition_name(self):
"""Return the composition name."""
name, _ = os.path.splitext(self._prj_cfg['ProjectInfo']['compositionName'])
return name
def get_composition_ending(self):
"""Return the composition ending."""
_, ending = os.path.splitext(self._prj_cfg['ProjectInfo']['compositionName'])
if ending:
return ending
return 'yml'
def get_composition_arxml(self):
"""Return the relative composition arxml path."""
return self._prj_cfg['ProjectInfo']['compositionArxml']
def get_gen_ext_impl_type(self):
"""Return the generate external implementation type."""
return self._prj_cfg['ProjectInfo'].get('generateExternalImplementationType', True)
def get_swc_name(self):
"""Returns the software component name."""
a2lname = f"{self.get_a2l_cfg()['name']}_SC"
return self._prj_cfg['ProjectInfo'].get('softwareComponentName', a2lname)
def get_car_com_dst(self):
"""Return the absolute path to the source output folder."""
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['didCarCom']))
def get_reports_dst_dir(self):
"""Get the destination dir for build reports.
Returns:
A path to the report files destination directory (with wildcards)
"""
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['reportDstDir']))
def get_all_reports_dst_dir(self):
"""Get the destination dir for build reports.
Returns:
A path to the report files destination directory (for all projects)
"""
return os.path.join(self.get_root_dir(), "..", "..", "Reports")
def get_log_dst_dir(self):
"""Return the absolute path to the log output folder.
Returns:
A path to the log files destination directory (with wildcards)
"""
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['logDstDir']))
def get_core_dummy_name(self):
"""Return the file name of the core dummy file from the config file.
Returns:
A file name for the core dummy files
"""
path = os.path.join(self.get_src_code_dst_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['coreDummyFileName']))
return path
def get_feature_conf_header_name(self):
"""Return the feature configuration header file name.
Returns:
A file name for the feature config header file
"""
return self._prj_cfg['ProjectInfo']['featureHeaderName']
def get_ts_header_name(self):
"""Return the name of the ts header file, defined in the config file.
Returns:
The file name of the file defining all unit raster times
"""
return self._prj_cfg['ProjectInfo']['tsHeaderName']
def get_included_units(self):
"""Return a list of all the included units in the project.
TODO:Consider moving this to the Feature Configs class if we start
using our configuration tool for model inclusion and scheduling
TODO:Consider calculate this on init and storing the result in the
class. this method would the just return the stored list.
"""
units_dict = self._unit_cfg['Rasters']
units = []
for unit in units_dict.values():
units.extend(unit)
return units
def get_included_common_files(self):
"""Return a list of all the included common files in the project.
Returns:
included_common_files ([str]): The names of the common files which are included in the project.
"""
return self._prj_cfg.get('includedCommonFiles', [])
def _calc_all_units(self):
"""Return a list of all the units."""
units = set()
for runits in self._unit_cfg['Rasters'].values():
units = units.union(set(runits))
self._all_units = list(units)
def get_includes_paths_flat(self):
"""Return list of paths to files to be included flat in source directory."""
includes_paths = self._prj_cfg.get('includesPaths', [])
return [os.path.join(self.get_root_dir(), os.path.normpath(path)) for path in includes_paths]
def get_includes_paths_tree(self):
"""Return list of paths to files to included with directories in source directory."""
includes_paths_tree = self._prj_cfg.get('includesPathsTree', [])
return [os.path.join(self.get_root_dir(), os.path.normpath(path)) for path in includes_paths_tree]
def get_all_units(self):
"""Return a list of all the units."""
return self._all_units
def get_prj_cfg_dir(self):
"""Return the directory containing the project configuration files.
Returns:
An absolute path to the project configuration files
"""
return os.path.join(self._prj_root_dir,
self._prj_cfg['ProjectInfo']['configDir'])
def get_scheduler_prefix(self):
"""Returns a prefix used to distinguish function calls in one project from
similarly named functions in other projects, when linked/compiled together
Returns:
scheduler_prefix (string): prefix for scheduler functions.
"""
return self._scheduler_prefix
def get_local_defs_name(self):
"""Return a string which defines the file name of local defines.
Returns:
A string containing the wildcard file name local defines
"""
return self._prj_cfg['ProjectInfo']['prjLocalDefs']
def get_codeswitches_name(self):
"""Return a string which defines the file name of code switches.
Returns:
A string containing the wildcard file name code switches
"""
return self._prj_cfg['ProjectInfo']['prjCodeswitches']
def get_did_cfg_file_name(self):
"""Return the did definition file name.
Returns:
DID definition file name
"""
return self._prj_cfg['ProjectInfo']['didDefFile']
def get_prj_config(self):
"""Get the project configuration name from the config file.
Returns:
Project config name
"""
return self._prj_cfg['ProjectInfo']["projConfig"]
def get_a2l_name(self):
"""Get the name of the a2l-file, which the build system shall generate."""
return self._prj_cfg['ProjectInfo']['a2LFileName']
def get_ecu_info(self):
"""Return ecuSupplier and ecuType.
Returns:
(ecuSupplier, ecuType)
"""
return (self._prj_cfg['ProjectInfo']['ecuSupplier'],
self._prj_cfg['ProjectInfo'].get('ecuType', ''))
def get_xcp_enabled(self):
"""Return True/False whether XCP is enabled in the project or not.
Returns:
(bool): True/False whether XCP is enabled in the project or not
"""
return self._prj_cfg['ProjectInfo'].get('enableXcp', True)
def get_nvm_defs(self):
"""Return NVM-ram block definitions.
The definitions contains the sizes of the six NVM areas
which are defined in the build-system.
Returns:
NvmConfig dict from config file.
"""
return self._prj_cfg['NvmConfig']
def _get_inc_dirs(self, path):
"""Get the dirs with the models defined in the units config file.
Model name somewhere in the path.
"""
all_dirs = glob.glob(path)
inc_units = self.get_included_units()
psep = os.path.sep
out = {}
for dir_ in all_dirs:
folders = dir_.split(psep)
for inc_unit in inc_units:
if inc_unit in folders:
out.update({inc_unit: dir_})
break
return out
def get_units_raster_cfg(self):
"""Get the units' scheduling raster config.
I.e. which units are included, and in which
rasters they are scheduled, and in which order.
Returns:
A dict in the following format.
::
{
"SampleTimes": {
"NameOfRaster": scheduling time},
"Rasters": {
"NameOfRaster": [
"NameOfFunction",
...],
...}
}
Example::
{
"SampleTimes": {
"Vc10ms": 0.01,
"Vc40ms": 0.04},
"Rasters": {
"Vc10ms": [
"VcPpmImob",
"VcPpmPsm",
"VcPpmRc",
"VcPpmSt",
"VcPemAlc"],
"Vc40ms": [
"VcRegCh"]
}
"""
return self._unit_cfg
def get_unit_cfg_dirs(self):
"""Get config dirs which matches the project config parameter prjUnitCfgDir.
Furthermore, they should be included in the unit definition for this project
Returns:
A list with absolute paths to all unit config dirs
included in the project
"""
path = os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['prjUnitCfgDir']))
return self._get_inc_dirs(path)
def get_translation_files_dirs(self):
"""Get translation files directories, specified as a path regex in project
config by key prjTranslationDir. If key is not present, will fall back to
prjUnitCfgDir.
Returns:
A dictionary with absolute paths to all translation file dirs included
in the project
"""
if "prjTranslationDir" not in self._prj_cfg['ProjectInfo']:
return self.get_unit_cfg_dirs()
normpath_dir = os.path.normpath(self._prj_cfg['ProjectInfo']['prjTranslationDir'])
path = os.path.join(self.get_root_dir(), normpath_dir)
all_dirs = glob.glob(path)
translation_dirs = {}
for directory in all_dirs:
file = pathlib.Path(directory).stem
translation_dirs[file] = directory
return translation_dirs
def get_common_src_dir(self):
"""Get source dir which matches the project config parameter commonSrcDir.
Returns:
Absolute path to common source dir
"""
return os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']['commonSrcDir']))
def get_unit_src_dirs(self):
"""Get source dirs which matches the project config parameter prjUnitCfgDir.
Furthermore, they should be included in the unit definition for this project
Returns:
A list with absolute paths to all source dirs included in the
project
"""
path = os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['prjUnitSrcDir']))
return self._get_inc_dirs(path)
def get_unit_mdl_dirs(self):
"""Get source dirs which matches the project config parameter prjUnitCfgDir.
Furthermore, they should be included in the unit definition for this project
Returns:
A list with absolute paths to all model dirs included in the
project
"""
path = os.path.join(self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']
['prjUnitMdlDir']))
return self._get_inc_dirs(path)
def get_use_global_const(self):
"""Get the name of the global constant module."""
return self._prj_cfg['ProjectInfo']['useGlobalConst']
def get_use_volatile_globals(self):
"""Get if global variables should be defined as volatile or not."""
if 'useVolatileGlobals' in self._prj_cfg['ProjectInfo']:
return self._prj_cfg['ProjectInfo']['useVolatileGlobals']
return False
def get_use_custom_dummy_spm(self):
"""Get path to file defining missing internal variables, if any.
This file will be used instead of generating VcDummy_spm.c,
to make it easier to maintain missing internal signals.
Returns:
customDummySpm (os.path): An absolute path to the custom dummy spm file, if existent.
"""
if 'customDummySpm' in self._prj_cfg['ProjectInfo']:
return os.path.join(
self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']['customDummySpm'])
)
return None
def get_use_custom_sources(self):
"""Get path to files with custom handwritten sourcecode, if any.
Returns:
customSources (os.path): A list of absolute paths to custom sources, if existent.
"""
if 'customSources' in self._prj_cfg['ProjectInfo']:
normalized_paths = (os.path.normpath(p) for p in self._prj_cfg['ProjectInfo']['customSources'])
return [os.path.join(self.get_root_dir(), p) for p in normalized_paths]
return None
def get_if_cfg_dir(self):
"""Return the directory containing the interface configuration files.
Returns:
An absolute path to the interface configuration files
"""
return os.path.join(self._prj_root_dir,
self._prj_cfg['ProjectInfo']['interfaceCfgDir'])
def get_enum_def_dir(self):
"""Get path to dir containing simulink enumeration definitions, if any.
Returns:
enumDefDir (os.path): An absolute path to the simulink enumerations, if existent.
"""
if 'enumDefDir' in self._prj_cfg['ProjectInfo']:
return os.path.join(
self.get_root_dir(),
os.path.normpath(self._prj_cfg['ProjectInfo']['enumDefDir'])
)
return None
if __name__ == '__main__':
# Function for testing the module
BPC = BuildProjConfig('../../ProjectCfg.json')