mbed-os/tools/config/__init__.py

1587 lines
63 KiB
Python
Executable File

"""
mbed SDK
Copyright (c) 2016-2020 ARM Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from __future__ import print_function, division, absolute_import
from copy import deepcopy
from six import moves
import os
import re
from os.path import dirname, abspath, exists, join, isabs
from collections import namedtuple
from os.path import relpath
from jinja2 import FileSystemLoader, StrictUndefined
from jinja2.environment import Environment
from jsonschema import Draft4Validator, RefResolver
from ..resources import FileType
from ..utils import (json_file_to_dict, intelhex_offset, integer,
NotSupportedException)
from ..arm_pack_manager import Cache
from ..targets import (CUMULATIVE_ATTRIBUTES, TARGET_MAP, generate_py_target,
get_resolution_order, Target)
from ..settings import DELIVERY_DIR
try:
unicode
except NameError:
unicode = str
PATH_OVERRIDES = set([
"target.bootloader_img",
"target.delivery_dir"
])
DELIVERY_OVERRIDES = set([
"target.deliver_to_target",
"target.deliver_artifacts",
"target.delivery_dir"
])
ROM_OVERRIDES = set([
# managed BL
"target.bootloader_img", "target.restrict_size",
"target.header_format", "target.header_offset",
"target.app_offset",
# unmanaged BL
"target.mbed_app_start", "target.mbed_app_size",
# both
"target.mbed_rom_start", "target.mbed_rom_size",
])
RAM_OVERRIDES = set([
# both
"target.mbed_ram_start", "target.mbed_ram_size",
])
BOOTLOADER_OVERRIDES = ROM_OVERRIDES | RAM_OVERRIDES | DELIVERY_OVERRIDES
ALLOWED_FEATURES = [
"BOOTLOADER", "BLE", "LWIP", "STORAGE", "NANOSTACK", "CRYPTOCELL310", "PSA",
"EXPERIMENTAL_API",
]
# List of all possible ram memories that can be available for a target
RAM_ALL_MEMORIES = ['IRAM1', 'IRAM2', 'IRAM3', 'IRAM4', 'SRAM_OC',
'SRAM_ITC', 'SRAM_DTC', 'SRAM_UPPER', 'SRAM_LOWER',
'SRAM']
# List of all possible rom memories that can be available for a target
ROM_ALL_MEMORIES = ['IROM1', 'PROGRAM_FLASH', 'IROM2']
# Base class for all configuration exceptions
class ConfigException(Exception):
"""Config system only exception. Makes it easier to distinguish config
errors"""
pass
class UndefinedParameter(ConfigException):
def __init__(self, param, name, kind, label):
self.param = param
self.name = name
self.kind = kind
self.label = label
def __str__(self):
return "Attempt to override undefined parameter '{}' in '{}'".format(
self.param,
ConfigParameter.get_display_name(self.name, self.kind, self.label),
)
class ConfigParameter(object):
"""This class keeps information about a single configuration parameter"""
def __init__(self, name, data, unit_name, unit_kind):
"""Construct a ConfigParameter
Positional arguments:
name - the name of the configuration parameter
data - the data associated with the configuration parameter
unit_name - the unit (target/library/application) that defines this
parameter
unit_kind - the kind of the unit ("target", "library" or "application")
"""
self.name = self.get_full_name(name, unit_name, unit_kind,
allow_prefix=False)
self.defined_by = self.get_display_name(unit_name, unit_kind)
self.set_value(data.get("value", None), unit_name, unit_kind)
self.value_min = data.get("value_min")
self.value_max = data.get("value_max")
self.accepted_values = data.get("accepted_values")
self.help_text = data.get("help", None)
self.required = data.get("required", False)
self.conflicts = data.get("conflicts", [])
self.macro_name = data.get("macro_name", "MBED_CONF_%s" %
self.sanitize(self.name.upper()))
self.config_errors = []
@staticmethod
def get_full_name(name, unit_name, unit_kind, label=None,
allow_prefix=True):
"""Return the full (prefixed) name of a parameter. If the parameter
already has a prefix, check if it is valid
Positional arguments:
name - the simple (unqualified) name of the parameter
unit_name - the unit (target/library/application) that defines this
parameter
unit_kind - the kind of the unit ("target", "library" or "application")
Keyword arguments:
label - the name of the label in the 'target_config_overrides' section
allow_prefix - True to allow the original name to have a prefix, False
otherwise
"""
# the name is not prefixed
if name.find('.') == -1:
if unit_kind == "target":
prefix = "target."
elif unit_kind == "application":
prefix = "app."
else:
prefix = unit_name + '.'
return prefix + name
if name in BOOTLOADER_OVERRIDES:
return name
# The name has a prefix, so check if it is valid
if not allow_prefix:
raise ConfigException("Invalid parameter name '%s' in '%s'" %
(name, ConfigParameter.get_display_name(
unit_name, unit_kind, label)))
temp = name.split(".")
# Check if the parameter syntax is correct (must be
# unit_name.parameter_name)
if len(temp) != 2:
raise ConfigException("Invalid parameter name '%s' in '%s'" %
(name, ConfigParameter.get_display_name(
unit_name, unit_kind, label)))
prefix = temp[0]
# Check if the given parameter prefix matches the expected prefix
if (
(unit_kind == "library" and prefix not in [unit_name, "target"]) or
(unit_kind == "target" and prefix != "target")
):
raise ConfigException(
"Invalid prefix '%s' for parameter name '%s' in '%s'" %
(prefix, name, ConfigParameter.get_display_name(
unit_name, unit_kind, label)))
return name
@staticmethod
def get_display_name(unit_name, unit_kind, label=None):
"""Return the name displayed for a unit when interrogating the origin
and the last set place of a parameter
Positional arguments:
unit_name - the unit (target/library/application) that defines this
parameter
unit_kind - the kind of the unit ("target", "library" or "application")
Keyword arguments:
label - the name of the label in the 'target_config_overrides' section
"""
if unit_kind == "target":
return "target:" + unit_name
elif unit_kind == "application":
return "application%s" % ("[%s]" % label if label else "")
else: # library
return "library:%s%s" % (
unit_name,
"[%s]" % label if label else ""
)
@staticmethod
def sanitize(name):
""" "Sanitize" a name so that it is a valid C macro name. Currently it
simply replaces '.' and '-' with '_'.
Positional arguments:
name - the name to make into a valid C macro
"""
return name.replace('.', '_').replace('-', '_')
def set_value(self, value, unit_name, unit_kind, label=None):
""" Sets a value for this parameter, remember the place where it was
set. If the value is a Boolean, it is converted to 1 (for True) or
to 0 (for False).
Positional arguments:
value - the value of the parameter
unit_name - the unit (target/library/application) that defines this
parameter
unit_kind - the kind of the unit ("target", "library" or "application")
Keyword arguments:
label - the name of the label in the 'target_config_overrides' section
(optional)
"""
self.value = int(value) if isinstance(value, bool) else value
self.set_by = self.get_display_name(unit_name, unit_kind, label)
def __str__(self):
"""Return the string representation of this configuration parameter
Arguments: None
"""
if self.value is not None:
return '%s = %s (macro name: "%s")' % \
(self.name, self.value, self.macro_name)
else:
return '%s has no value' % self.name
def get_verbose_description(self):
"""Return a verbose description of this configuration parameter as a
string
Arguments: None
"""
desc = "Name: %s%s\n" % \
(self.name, " (required parameter)" if self.required else "")
if self.help_text:
desc += " Description: %s\n" % self.help_text
desc += " Defined by: %s\n" % self.defined_by
if not self.value:
return desc + " No value set"
desc += " Macro name: %s\n" % self.macro_name
desc += " Value: %s (set by %s)" % (self.value, self.set_by)
if self.conflicts:
desc += " Conflicts with %s" % ", ".join(self.conflicts)
return desc
class ConfigMacro(object):
""" A representation of a configuration macro. It handles both macros
without a value (MACRO) and with a value (MACRO=VALUE)
"""
def __init__(self, name, unit_name, unit_kind):
"""Construct a ConfigMacro object
Positional arguments:
name - the macro's name
unit_name - the location where the macro was defined
unit_kind - the type of macro this is
"""
self.name = name
self.defined_by = ConfigParameter.get_display_name(
unit_name, unit_kind
)
if name.find("=") != -1:
tmp = name.split("=")
if len(tmp) != 2:
raise ValueError("Invalid macro definition '%s' in '%s'" %
(name, self.defined_by))
self.macro_name = tmp[0]
self.macro_value = tmp[1]
else:
self.macro_name = name
self.macro_value = None
class ConfigCumulativeOverride(object):
"""Representation of overrides for cumulative attributes"""
def __init__(self, name, additions=None, removals=None, strict=False):
"""Construct a ConfigCumulativeOverride object
Positional arguments:
name - the name of the config file this came from ?
Keyword arguments:
additions - macros to add to the overrides
removals - macros to remove from the overrides
strict - Boolean indicating that attempting to remove from an override
that does not exist should error
"""
self.name = name
if additions:
self.additions = set(additions)
else:
self.additions = set()
if removals:
self.removals = set(removals)
else:
self.removals = set()
self.strict = strict
def remove_cumulative_overrides(self, overrides):
"""Extend the list of override removals.
Positional arguments:
overrides - a list of names that, when the override is evaluated, will
be removed
"""
for override in overrides:
if override in self.additions:
raise ConfigException(
"Configuration conflict. The %s %s both added and removed."
% (self.name[:-1], override))
self.removals |= set(overrides)
def add_cumulative_overrides(self, overrides):
"""Extend the list of override additions.
Positional arguments:
overrides - a list of a names that, when the override is evaluated,
will be added to the list
"""
for override in overrides:
if override in self.removals or \
(self.strict and override not in self.additions):
raise ConfigException(
"Configuration conflict. The %s %s both added and removed."
% (self.name[:-1], override))
self.additions |= set(overrides)
def strict_cumulative_overrides(self, overrides):
"""Remove all overrides that are not the specified ones
Positional arguments:
overrides - a list of names that will replace the entire attribute when
this override is evaluated.
"""
self.remove_cumulative_overrides(self.additions - set(overrides))
self.add_cumulative_overrides(overrides)
self.strict = True
def update_target(self, target):
"""Update the attributes of a target based on this override"""
setattr(target, self.name,
list((set(getattr(target, self.name, []))
| self.additions) - self.removals))
def _process_config_parameters(data, params, unit_name, unit_kind):
"""Process a "config_parameters" section in either a target, a library,
or the application.
Positional arguments:
data - a dictionary with the configuration parameters
params - storage for the discovered configuration parameters
unit_name - the unit (target/library/application) that defines this
parameter
unit_kind - the kind of the unit ("target", "library" or "application")
"""
for name, val in data.items():
full_name = ConfigParameter.get_full_name(name, unit_name, unit_kind)
# If the parameter was already defined, raise an error
if full_name in params:
raise ConfigException(
"Parameter name '%s' defined in both '%s' and '%s'" %
(name, ConfigParameter.get_display_name(unit_name, unit_kind),
params[full_name].defined_by))
# Otherwise add it to the list of known parameters
# If "val" is not a dictionary, this is a shortcut definition,
# otherwise it is a full definition
params[full_name] = ConfigParameter(name, val if isinstance(val, dict)
else {"value": val}, unit_name,
unit_kind)
return params
def _process_macros(mlist, macros, unit_name, unit_kind):
"""Process a macro definition and check for incompatible duplicate
definitions.
Positional arguments:
mlist - list of macro names to process
macros - dictionary with currently discovered macros
unit_name - the unit (library/application) that defines this macro
unit_kind - the kind of the unit ("library" or "application")
"""
for mname in mlist:
macro = ConfigMacro(mname, unit_name, unit_kind)
if (macro.macro_name in macros) and \
(macros[macro.macro_name].name != mname):
# Found an incompatible definition of the macro in another module,
# so raise an error
full_unit_name = ConfigParameter.get_display_name(unit_name,
unit_kind)
raise ConfigException(
("Macro '%s' defined in both '%s' and '%s'"
% (macro.macro_name, macros[macro.macro_name].defined_by,
full_unit_name)) +
" with incompatible values")
macros[macro.macro_name] = macro
Region = namedtuple("Region", "name start size active filename")
RamRegion = namedtuple("RamRegion", "name start size active")
class Config(object):
"""'Config' implements the mbed configuration mechanism"""
# Libraries and applications have different names for their configuration
# files
__mbed_app_config_name = "mbed_app.json"
__mbed_lib_config_name = "mbed_lib.json"
__unused_overrides = set(["target.bootloader_img", "target.restrict_size",
"target.mbed_app_start", "target.mbed_app_size"])
@classmethod
def find_app_config(cls, top_level_dirs):
app_config_location = None
for directory in top_level_dirs:
full_path = os.path.join(directory, cls.__mbed_app_config_name)
if os.path.isfile(full_path):
if app_config_location is not None:
raise ConfigException(
"Duplicate '{}' file in '{}' and '{}'".format(
cls.__mbed_app_config_name,
cls.app_config_location,
full_path
)
)
else:
app_config_location = full_path
return app_config_location
def format_validation_error(self, error, path):
if error.context:
return self.format_validation_error(error.context[0], path)
else:
return "in {} element {}: {}".format(
path, ".".join(p for p in error.absolute_path),
error.message.replace('u\'', '\''))
def __init__(self, tgt, top_level_dirs=None, app_config=None):
"""Construct a mbed configuration
Positional arguments:
target - the name of the mbed target used for this configuration
instance
Keyword argumets:
top_level_dirs - a list of top level source directories (where
mbed_app_config.json could be found)
app_config - location of a chosen mbed_app.json file
NOTE: Construction of a Config object will look for the application
configuration file in top_level_dirs. If found once, it'll parse it.
top_level_dirs may be None (in this case, the constructor will not
search for a configuration file).
"""
config_errors = []
self.config_errors = []
self.app_config_location = app_config
if self.app_config_location is None and top_level_dirs:
self.app_config_location = self.find_app_config(top_level_dirs)
try:
if self.app_config_location:
self.app_config_data = json_file_to_dict(
self.app_config_location
)
else:
self.app_config_data = {}
except ValueError:
self.app_config_data = {}
config_errors.append(
ConfigException(
"Could not parse mbed app configuration from %s"
% self.app_config_location
)
)
if self.app_config_location is not None:
# Validate the format of the JSON file based on schema_app.json
schema_root = os.path.dirname(os.path.abspath(__file__))
schema_path = os.path.join(schema_root, "schema_app.json")
schema = json_file_to_dict(schema_path)
url = moves.urllib.request.pathname2url(schema_path)
uri = moves.urllib_parse.urljoin("file://", url)
resolver = RefResolver(uri, schema)
validator = Draft4Validator(schema, resolver=resolver)
errors = sorted(
validator.iter_errors(self.app_config_data), key=str
)
if errors:
raise ConfigException("; ".join(
self.format_validation_error(x, self.app_config_location)
for x in errors))
# Update the list of targets with the ones defined in the application
# config, if applicable
self.lib_config_data = {}
# Make sure that each config is processed only once
self.processed_configs = {}
if isinstance(tgt, Target):
self.target = tgt
else:
if tgt in TARGET_MAP:
self.target = TARGET_MAP[tgt]
else:
self.target = generate_py_target(
self.app_config_data.get("custom_targets", {}), tgt)
self.target = deepcopy(self.target)
self.target_labels = self.target.labels
po_without_target = set(o.split(".")[1] for o in PATH_OVERRIDES)
for override in BOOTLOADER_OVERRIDES:
_, attr = override.split(".")
if not hasattr(self.target, attr):
setattr(self.target, attr, None)
elif attr in po_without_target:
new_path = join(
dirname(self.target._from_file),
getattr(self.target, attr)
)
setattr(self.target, attr, new_path)
self.cumulative_overrides = {key: ConfigCumulativeOverride(key)
for key in CUMULATIVE_ATTRIBUTES}
self._process_config_and_overrides(self.app_config_data, {}, "app",
"application")
self.config_errors = config_errors
def add_config_files(self, flist):
"""Add configuration files
Positional arguments:
flist - a list of files to add to this configuration
"""
for config_file in flist:
if not config_file.endswith(self.__mbed_lib_config_name):
continue
full_path = os.path.normpath(os.path.abspath(config_file))
# Check that we didn't already process this file
if full_path in self.processed_configs:
continue
# Read the library configuration and add a "__full_config_path"
# attribute to it
try:
cfg = json_file_to_dict(config_file)
except ValueError as exc:
raise ConfigException(str(exc))
# Validate the format of the JSON file based on the schema_lib.json
schema_root = os.path.dirname(os.path.abspath(__file__))
schema_path = os.path.join(schema_root, "schema_lib.json")
schema_file = json_file_to_dict(schema_path)
url = moves.urllib.request.pathname2url(schema_path)
uri = moves.urllib_parse.urljoin("file://", url)
resolver = RefResolver(uri, schema_file)
validator = Draft4Validator(schema_file, resolver=resolver)
errors = sorted(validator.iter_errors(cfg), key=str)
if errors:
raise ConfigException("; ".join(
self.format_validation_error(x, config_file)
for x in errors))
if "requires" in self.app_config_data:
if cfg["name"] not in self.app_config_data["requires"]:
continue
self.app_config_data["requires"].extend(
cfg.get("requires", [])
)
self.processed_configs[full_path] = True
cfg["__config_path"] = full_path
# If there's already a configuration for a module with the same
# name, exit with error
if cfg["name"] in self.lib_config_data:
raise ConfigException(
"Library name '{}' is not unique "
"(defined in '{}' and '{}')".format(
cfg["name"],
full_path,
self.lib_config_data[cfg["name"]]["__config_path"]
)
)
self.lib_config_data[cfg["name"]] = cfg
@property
def has_regions(self):
"""Does this config have regions defined?"""
for override in ROM_OVERRIDES:
_, attr = override.split(".")
if getattr(self.target, attr, None):
return True
return False
@property
def has_ram_regions(self):
"""Does this config have regions defined?"""
for override in RAM_OVERRIDES:
_, attr = override.split(".")
if getattr(self.target, attr, None):
return True
return False
def deliver_into(self):
if self.target.deliver_to_target:
if self.target.delivery_dir:
target_delivery_dir = self.target.delivery_dir
else:
label_dir = "TARGET_{}".format(self.target.deliver_to_target)
target_delivery_dir = join(DELIVERY_DIR, label_dir)
if not exists(target_delivery_dir):
os.makedirs(target_delivery_dir)
return target_delivery_dir, self.target.deliver_artifacts
else:
return None, None
@property
def sectors(self):
"""Return a list of tuples of sector start,size"""
try:
return self.target.sectors
except AttributeError:
cache = Cache(False, False)
if self.target.device_name not in cache.index:
raise ConfigException(
"Bootloader not supported on this target: "
"targets.json `device_name` not found in "
"arm_pack_manager index."
)
cmsis_part = cache.index[self.target.device_name]
sectors = cmsis_part['sectors']
if sectors:
return sectors
raise ConfigException("No sector info available")
def _get_cmsis_part(self):
if not hasattr(self.target, "device_name"):
raise ConfigException("Bootloader not supported on this target: "
"targets.json `device_name` not specified.")
cache = Cache(False, False)
if self.target.device_name not in cache.index:
raise ConfigException("Bootloader not supported on this target: "
"targets.json `device_name` not found in "
"arm_pack_manager index.")
return cache.index[self.target.device_name]
@staticmethod
def _memory_ordering(memory):
return (memory['default'], memory['size'], memory['start'])
def _get_mem_specs(self, permissions, cmsis_part):
all_matching_memories = {
name: memory for name, memory in cmsis_part['memories'].items()
if all(memory['access'].get(perm) for perm in permissions)
}
if all_matching_memories:
return all_matching_memories
else:
raise ConfigException(
"Missing a memory that is {} in CMSIS Pack data".format(
", ".join(permissions)
)
)
def _get_primary_memory_override(self, memory_type):
mem_start = None
mem_size = None
if hasattr(self.target, "mbed_{}_start".format(memory_type)):
mem_start = getattr(
self.target,
"mbed_{}_start".format(memory_type)
)
if hasattr(self.target, "mbed_{}_size".format(memory_type)):
mem_size = getattr(self.target, "mbed_{}_size".format(memory_type))
if mem_start and not isinstance(mem_start, int):
mem_start = int(mem_start, 0)
if mem_size and not isinstance(mem_size, int):
mem_size = int(mem_size, 0)
return mem_start, mem_size
def get_all_active_memories(self, memory_list):
"""Get information of all available rom/ram memories in the form of
dictionary {Memory: [start_addr, size]}. Takes in the argument, a
list of all available regions within the ram/rom memory
"""
# Override rom_start/rom_size
#
# This is usually done for a target which:
# 1. Doesn't support CMSIS pack, or
# 2. Supports TrustZone and user needs to change its flash partition
available_memories = {}
# Counter to keep track of ROM/RAM memories supported by target
active_memory_counter = 0
# Find which memory we are dealing with, RAM/ROM
if any('ROM' in mem_list for mem_list in memory_list):
active_memory = 'ROM'
else:
active_memory = 'RAM'
try:
cmsis_part = self._get_cmsis_part()
present_memories = set(cmsis_part['memories'].keys())
valid_memories = set(memory_list).intersection(present_memories)
memories = self._get_mem_specs(
["read", "write" if active_memory == "RAM" else "execute"],
cmsis_part
)
for memory in valid_memories:
mem_start = memories[memory]["start"]
mem_size = memories[memory]["size"]
if memory in ['IROM1', 'PROGRAM_FLASH']:
start, size = self._get_primary_memory_override("rom")
if start:
mem_start = start
if size:
mem_size = size
memory = 'ROM'
elif memory in ['IRAM1', 'SRAM_OC', 'SRAM_UPPER', 'SRAM']:
start, size = self._get_primary_memory_override("ram")
if start:
mem_start = start
if size:
mem_size = size
memory = 'RAM'
else:
active_memory_counter += 1
memory = active_memory + str(active_memory_counter)
if not isinstance(mem_start, int):
mem_start = int(mem_start, 0)
if not isinstance(mem_size, int):
mem_size = int(mem_size, 0)
available_memories[memory] = [mem_start, mem_size]
if not available_memories:
raise ConfigException(
"Bootloader not supported on this target. "
"No memories found."
)
return available_memories
except ConfigException:
""" If the target doesn't exits in cmsis, but present in targets.json
with ram and rom start/size defined"""
start, size = self._get_primary_memory_override(
active_memory.lower()
)
if start is None:
raise ConfigException(
"Bootloader not supported on this target. {} "
"start not found in targets.json.".format(active_memory)
)
if size is None:
raise ConfigException(
"Bootloader not supported on this target. {} "
"size not found in targets.json.".format(active_memory)
)
available_memories[active_memory] = [start, size]
return available_memories
@property
def ram_regions(self):
"""Generate a list of ram regions from the config"""
cmsis_part = self._get_cmsis_part()
rams = self._get_mem_specs(("read", "write"), cmsis_part)
best_ram = sorted(
rams.values(),
key=self._memory_ordering,
reverse=True
)[0]
ram_start, ram_size = best_ram["start"], best_ram["size"]
# Override ram_start/ram_size
#
# This is usually done for a target which:
# 1. Doesn't support CMSIS pack, or
# 2. Supports TrustZone and user needs to change its flash partition
if getattr(self.target, "mbed_ram_start"):
ram_start = int(getattr(self.target, "mbed_ram_start"), 0)
if getattr(self.target, "mbed_ram_size"):
ram_size = int(getattr(self.target, "mbed_ram_size"), 0)
return [RamRegion("application_ram", ram_start, ram_size, True)]
@property
def regions(self):
if not getattr(self.target, "bootloader_supported", False):
raise ConfigException("Bootloader not supported on this target.")
"""Generate a list of regions from the config"""
if (
(self.target.bootloader_img or self.target.restrict_size) and
(self.target.mbed_app_start or self.target.mbed_app_size)
):
raise ConfigException(
"target.bootloader_img and target.restrict_size are "
"incompatible with target.mbed_app_start and "
"target.mbed_app_size")
rom = self.get_all_active_memories(ROM_ALL_MEMORIES)
if self.target.bootloader_img or self.target.restrict_size:
return self._generate_bootloader_build(rom)
else:
return self._generate_linker_overrides(rom)
@staticmethod
def header_member_size(member):
_, _, subtype, _ = member
try:
return int(subtype[:-2]) // 8
except Exception:
if subtype.startswith("CRCITT32"):
return 32 // 8
elif subtype == "SHA256":
return 256 // 8
elif subtype == "SHA512":
return 512 // 8
else:
raise ValueError("target.header_format: subtype %s is not "
"understood" % subtype)
@staticmethod
def _header_size(format):
return sum(Config.header_member_size(m) for m in format)
def _make_header_region(self, start, header_format, offset=None):
size = self._header_size(header_format)
region = Region("header", start, size, False, None)
start += size
start = ((start + (2**7 - 1)) // (2**7)) * (2**7)
return (start, region)
@staticmethod
def _assign_new_offset(rom_start, new_offset, region_name, regions):
newstart = rom_start + integer(new_offset, 0)
for s, e in regions:
if newstart > s and newstart < e:
raise ConfigException(
"Can not place {} region inside previous region".format(
region_name
)
)
return newstart
@staticmethod
def _get_end_address(region_list, start_address, rom_end):
"""Given a start address and set of regions, sort the
regions and then compute the end address.
The end address is either rom_end or beginning of the
next section, whichever is smaller
"""
# Sort the list by starting address
region_list = sorted(region_list, key=lambda x: x[0])
for s, e in region_list:
if start_address < s:
return s
return rom_end
def _generate_bootloader_build(self, rom_memories):
rom_start, rom_size = rom_memories.get('ROM')
start = rom_start
rom_end = rom_start + rom_size
if self.target.bootloader_img:
if isabs(self.target.bootloader_img):
filename = self.target.bootloader_img
else:
basedir = abspath(dirname(self.app_config_location))
filename = join(basedir, self.target.bootloader_img)
if not exists(filename):
raise ConfigException("Bootloader %s not found" % filename)
part = intelhex_offset(filename, offset=rom_start)
if part.minaddr() != rom_start:
raise ConfigException("bootloader executable does not "
"start at 0x%x" % rom_start)
regions = part.segments()
# find the last valid address that's within rom_end and use that
# to compute the bootloader size
# we have multiple parts in bootloader. Treat each of them as
# a different region (BLP1, BLP2 ...)
if len(part.segments()) > 1:
end_address = None
part_count = 0
for start, stop in part.segments():
part_count += 1
if (stop < rom_end):
end_address = stop
else:
break
if end_address is None:
raise ConfigException(
"bootloader segments don't fit within rom"
)
part_size = Config._align_ceiling(
end_address, self.sectors
) - rom_start
# Generate the region in the loop
# (bootloader0, bootloader1, ...)
yield Region(
"bootloader{}".format(str(part_count)),
start,
part_size,
False,
filename
)
else:
# Number of segments is 1
_, end_address = part.segments()[0]
if (end_address > rom_end):
raise ConfigException(
"bootloader segments don't fit within rom"
)
part_size = Config._align_ceiling(
end_address, self.sectors
) - rom_start
yield Region("bootloader", rom_start, part_size, False,
filename)
start = rom_start + part_size
if self.target.header_format:
if self.target.header_offset:
start = self._assign_new_offset(
rom_start,
self.target.header_offset,
"header",
regions
)
start, region = self._make_header_region(
start, self.target.header_format)
yield region._replace(filename=self.target.header_format)
if self.target.restrict_size is not None:
if self.target.app_offset:
start = self._assign_new_offset(
rom_start,
self.target.app_offset,
"application",
regions
)
new_size = int(self.target.restrict_size, 0)
new_size = Config._align_floor(
start + new_size, self.sectors
) - start
yield Region("application", start, new_size, True, None)
start += new_size
if self.target.header_format and not self.target.bootloader_img:
if self.target.header_offset:
start = self._assign_new_offset(
rom_start,
self.target.header_offset,
"header",
regions
)
start, region = self._make_header_region(
start, self.target.header_format
)
yield region
yield Region("post_application", start, rom_end - start,
False, None)
else:
if self.target.app_offset:
start = self._assign_new_offset(
rom_start, self.target.app_offset, "application", regions)
# compute the end address of the application region based on
# existing segments
end = self._get_end_address(regions, start, rom_end)
yield Region("application", start, end - start,
True, None)
if start > rom_end:
raise ConfigException("Not enough memory on device to fit all "
"application regions")
@staticmethod
def _find_sector(address, sectors):
target_size = -1
target_start = -1
for (start, size) in sectors:
if address < start:
break
target_start = start
target_size = size
if (target_size < 0):
raise ConfigException("No valid sector found")
return target_start, target_size
@staticmethod
def _align_floor(address, sectors):
target_start, target_size = Config._find_sector(address, sectors)
sector_num = (address - target_start) // target_size
return target_start + (sector_num * target_size)
@staticmethod
def _align_ceiling(address, sectors):
target_start, target_size = Config._find_sector(address, sectors)
sector = ((address - target_start) + target_size - 1) // target_size
return target_start + (sector * target_size)
@property
def report(self):
return {
'app_config': self.app_config_location,
'library_configs': list(map(
relpath,
self.processed_configs.keys()
))
}
def _generate_linker_overrides(self, rom_memories):
rom_start, rom_size = rom_memories.get('ROM')
if self.target.mbed_app_start is not None:
start = int(self.target.mbed_app_start, 0)
else:
start = rom_start
if self.target.mbed_app_size is not None:
size = int(self.target.mbed_app_size, 0)
else:
size = (rom_size + rom_start) - start
if start < rom_start:
raise ConfigException("Application starts before ROM")
if size + start > rom_size + rom_start:
raise ConfigException("Application ends after ROM")
yield Region("application", start, size, True, None)
def _process_config_and_overrides(
self,
data,
params,
unit_name,
unit_kind
):
"""Process "config_parameters" and "target_config_overrides" into a
given dictionary
Positional arguments:
data - the configuration data of the library/appliation
params - storage for the discovered configuration parameters
unit_name - the unit (library/application) that defines this parameter
unit_kind - the kind of the unit ("library" or "application")
"""
_process_config_parameters(data.get("config", {}), params, unit_name,
unit_kind)
for label, overrides in data.get("target_overrides", {}).items():
# If the label is defined by the target or it has the special value
# "*", process the overrides
if (label == '*') or (label in self.target_labels):
# Check for invalid cumulative overrides in libraries
if (unit_kind == 'library' and
any(attr.startswith('target.extra_labels') for attr
in overrides.keys())):
raise ConfigException(
"Target override 'target.extra_labels' in " +
ConfigParameter.get_display_name(unit_name, unit_kind,
label) +
" is only allowed at the application level")
# Parse out cumulative overrides
for attr, cumulatives in self.cumulative_overrides.items():
if 'target.'+attr in overrides:
key = 'target.' + attr
if not isinstance(overrides[key], list):
raise ConfigException(
"The value of %s.%s is not of type %s" %
(unit_name, "target_overrides." + key,
"list"))
cumulatives.strict_cumulative_overrides(overrides[key])
del overrides[key]
if 'target.'+attr+'_add' in overrides:
key = 'target.' + attr + "_add"
if not isinstance(overrides[key], list):
raise ConfigException(
"The value of %s.%s is not of type %s" %
(unit_name, "target_overrides." + key,
"list"))
cumulatives.add_cumulative_overrides(overrides[key])
del overrides[key]
if 'target.'+attr+'_remove' in overrides:
key = 'target.' + attr + "_remove"
if not isinstance(overrides[key], list):
raise ConfigException(
"The value of %s.%s is not of type %s" %
(unit_name, "target_overrides." + key,
"list"))
cumulatives.remove_cumulative_overrides(overrides[key])
del overrides[key]
# Consider the others as overrides
for name, val in overrides.items():
if (name in PATH_OVERRIDES and "__config_path" in data):
val = os.path.join(
os.path.dirname(data["__config_path"]), val)
# Get the full name of the parameter
full_name = ConfigParameter.get_full_name(name, unit_name,
unit_kind, label)
if full_name in params:
params[full_name].set_value(val, unit_name, unit_kind,
label)
elif (
name.startswith("target.") and
(unit_kind == "application" or
name in BOOTLOADER_OVERRIDES)
):
_, attribute = name.split(".")
setattr(self.target, attribute, val)
continue
else:
self.config_errors.append(
UndefinedParameter(
full_name, unit_name, unit_kind, label))
for cumulatives in self.cumulative_overrides.values():
cumulatives.update_target(self.target)
return params
def get_target_config_data(self):
"""Read and interpret configuration data defined by targets.
We consider the resolution order for our target and sort it by level
reversed, so that we first look at the top level target (the parent),
then its direct children, then the children of those children and so
on, until we reach self.target
TODO: this might not work so well in some multiple inheritance
scenarios.
At each step, look at two keys of the target data:
- config_parameters: used to define new configuration parameters
- config_overrides: used to override already defined configuration
parameters
Arguments: None
"""
params, json_data = {}, self.target.json_data
resolution_order = [e[0] for e
in sorted(
self.target.resolution_order,
key=lambda e: e[1], reverse=True)]
for tname in resolution_order:
# Read the target data directly from its description
target_data = json_data[tname]
# Process definitions first
_process_config_parameters(target_data.get("config", {}), params,
tname, "target")
# Then process overrides
for name, val in target_data.get("overrides", {}).items():
full_name = ConfigParameter.get_full_name(
name, tname, "target"
)
# If the parameter name is not defined or if there isn't a path
# from this target to the target where the parameter was
# defined in the target inheritance tree, raise an error We
# need to use 'defined_by[7:]' to remove the "target:"
# prefix from defined_by
rel_names = [tgt for tgt, _ in
get_resolution_order(self.target.json_data, tname,
[])]
if full_name in BOOTLOADER_OVERRIDES:
continue
if (full_name not in params) or \
(params[full_name].defined_by[7:] not in rel_names):
raise UndefinedParameter(name, tname, "target", "")
# Otherwise update the value of the parameter
params[full_name].set_value(val, tname, "target")
return params
def get_lib_config_data(self, target_data):
""" Read and interpret configuration data defined by libraries. It is
assumed that "add_config_files" above was already called and the
library configuration data exists in self.lib_config_data
Arguments: target_data
"""
macros = {}
for lib_name, lib_data in self.lib_config_data.items():
self._process_config_and_overrides(
lib_data, target_data, lib_name, "library")
_process_macros(lib_data.get("macros", []), macros, lib_name,
"library")
return target_data, macros
def get_app_config_data(self, params, macros):
""" Read and interpret the configuration data defined by the target. The
target can override any configuration parameter, as well as define its
own configuration data.
Positional arguments.
params - the dictionary with configuration parameters found so far (in
the target and in libraries)
macros - the list of macros defined in the configuration
"""
app_cfg = self.app_config_data
# The application can have a "config_parameters" and a
# "target_config_overrides" section just like a library
self._process_config_and_overrides(app_cfg, params, "app",
"application")
# The application can also defined macros
_process_macros(app_cfg.get("macros", []), macros, "app",
"application")
def get_config_data(self):
""" Return the configuration data in two parts: (params, macros)
params - a dictionary with mapping a name to a ConfigParam
macros - the list of macros defined with "macros" in libraries and in
the application (as ConfigMacro instances)
Arguments: None
"""
all_params = self.get_target_config_data()
lib_params, macros = self.get_lib_config_data(all_params)
self.get_app_config_data(lib_params, macros)
return lib_params, macros
@staticmethod
def _check_required_parameters(params):
"""Check that there are no required parameters without a value
Positional arguments:
params - the list of parameters to check
NOTE: This function does not return. Instead, it throws a
ConfigException when any of the required parameters are missing values
"""
for param in params.values():
if param.required and (param.value is None):
raise ConfigException("Required parameter '" + param.name +
"' defined by '" + param.defined_by +
"' doesn't have a value")
@staticmethod
def _parameters_and_config_macros_to_macros(params, macros):
""" Return the macro definitions generated for a dictionary of
ConfigParameters and a dictionary of ConfigMacros (as returned by
get_config_data). The ConfigParameters override any matching macros set
by the ConfigMacros.
Positional arguments:
params - a dictionary mapping a name to a ConfigParameter
macros - a dictionary mapping a name to a ConfigMacro
Return: a list of strings that are the C pre-processor macros
"""
all_macros = {
m.macro_name: m.macro_value for m in macros.values()
}
parameter_macros = {
p.macro_name: p.value for p in params.values()
if p.value is not None
}
all_macros.update(parameter_macros)
macro_list = []
for name, value in all_macros.items():
# If the macro does not have a value, just append the name.
# Otherwise, append the macro as NAME=VALUE
if value is None:
macro_list.append(name)
else:
macro_list.append("%s=%s" % (name, value))
return macro_list
@staticmethod
def config_macros_to_macros(macros):
""" Return the macro definitions generated for a dictionary of
ConfigMacros (as returned by get_config_data).
Positional arguments:
params - a dictionary mapping a name to a ConfigMacro instance
Return: a list of strings that are the C pre-processor macros
"""
return [m.name for m in macros.values()]
@staticmethod
def config_to_macros(config):
"""Convert the configuration data to a list of C macros
Positional arguments:
config - configuration data as (ConfigParam instances, ConfigMacro
instances) tuple (as returned by get_config_data())
"""
params, macros = config[0], config[1]
Config._check_required_parameters(params)
return Config._parameters_and_config_macros_to_macros(params, macros)
def get_config_data_macros(self):
""" Convert a Config object to a list of C macros
Arguments: None
"""
return self.config_to_macros(self.get_config_data())
def get_features(self):
""" Extract any features from the configuration data
Arguments: None
"""
params, _ = self.get_config_data()
self._check_required_parameters(params)
self.cumulative_overrides['features']\
.update_target(self.target)
# Features that don't appear in ALLOWED_FEATURES should be removed
# with a warning so that they don't do anything unexpected.
# Iterate over a copy of the set to remove them safely.
for feature in list(self.target.features):
if feature not in ALLOWED_FEATURES:
print("[WARNING] Feature '%s' is not a supported feature" % feature)
self.target.features.remove(feature)
return self.target.features
def validate_config(self):
""" Validate configuration settings. This either returns True or
raises an exception
Arguments: None
"""
params, _ = self.get_config_data()
err_msg = ""
for name, param in sorted(params.items()):
min = param.value_min
max = param.value_max
accepted = param.accepted_values
value = param.value
# Config parameters that are only defined but do not have a default
# value should not be range limited
if value is not None:
if (
(min is not None or max is not None) and
(accepted is not None)
):
err_msg += (
"\n%s has both a range and list of accepted values "
"specified. Please only specify either value_min "
"and/or value_max, or accepted_values"
% param
)
else:
if re.match(r'^(0[xX])[A-Fa-f0-9]+$|^[0-9]+$', str(value)):
# Value is a hexadecimal or numerical string value
# Convert to a python integer and range check/compare
# to accepted list accordingly
if min is not None or max is not None:
# Numerical range check
# Convert hex strings to integers for range checks
value = int(str(value), 0)
min = int(str(min), 0) if min is not None else None
max = int(str(max), 0) if max is not None else None
if (
(min is not None and value < min) or
(max is not None and value > max)
):
err_msg += (
"\nInvalid config range for {}, is not in "
"the required range: [{}:{}]".format(
param,
min if min is not None else "-inf",
max if max is not None else "inf"
)
)
# Numerical accepted value check
elif accepted is not None and value not in accepted:
err_msg += (
"\nInvalid value for {}, is not an accepted "
"value: {}".format(
param,
", ".join(map(str, accepted))
)
)
else:
if min is not None or max is not None:
err_msg += (
"\nInvalid config range settings for {}. "
"Range specifiers are not applicable to "
"non-decimal/hexadecimal string values".format(
param
)
)
if accepted is not None and value not in accepted:
err_msg += (
"\nInvalid config range for {}, is not an "
"accepted value: {}".format(
param, ", ".join(accepted)
)
)
if (err_msg):
raise ConfigException(err_msg)
for error in self.config_errors:
if (
isinstance(error, UndefinedParameter) and
error.param in params
):
continue
else:
raise error
for param in params.values():
for conflict in param.conflicts:
if conflict in BOOTLOADER_OVERRIDES:
_, attr = conflict.split(".")
conf = ConfigParameter(
conflict, {"value": getattr(self.target, attr)},
"target", "target"
)
else:
conf = params.get(conflict)
if (
param.value and conf and conf.value
and param.value != conf.value
):
raise ConfigException(
("Configuration parameter {} with value {} conflicts "
"with {} with value {}").format(
param.name, param.value, conf.name, conf.value
)
)
return True
@property
def name(self):
if "artifact_name" in self.app_config_data:
return self.app_config_data["artifact_name"]
else:
return None
def load_resources(self, resources):
""" Load configuration data from a Resources instance and expand it
based on defined features.
Positional arguments:
resources - the resources object to load from and expand
"""
# Update configuration files until added features creates no changes
prev_features = set()
prev_requires = set()
while True:
# Add/update the configuration with any .json files found while
# scanning
self.add_config_files(
f.path for f in resources.get_file_refs(FileType.JSON)
)
# Add features while we find new ones
features = set(self.get_features())
requires = set(self.app_config_data.get("requires", []))
if features == prev_features and requires == prev_requires:
break
resources.add_features(features)
prev_features = features
prev_requires = requires
self.validate_config()
missing_requirements = {}
for name, lib in self.lib_config_data.items():
for req in lib.get("requires", []):
if req not in self.lib_config_data:
missing_requirements.setdefault(name, [])
missing_requirements[name].append(req)
if missing_requirements:
message = "; ".join(
"library '{}' requires {} which is not present".format(
name, ", ".join("'{}'".format(i) for i in missing)
)
for name, missing in missing_requirements.items()
)
raise ConfigException(message)
all_json_paths = [
cfg["__config_path"] for cfg in self.lib_config_data.values()
]
included_json_files = [
ref for ref in resources.get_file_refs(FileType.JSON)
if abspath(ref.path) in all_json_paths
]
resources.filter_by_libraries(included_json_files)
if (
hasattr(self.target, "release_versions") and
"5" not in self.target.release_versions and
"rtos" in self.lib_config_data
):
raise NotSupportedException("Target does not support mbed OS 5")
@staticmethod
def config_to_header(config, fname=None):
""" Convert the configuration data to the content of a C header file,
meant to be included to a C/C++ file. The content is returned as a
string.
Positional arguments:
config - configuration data as (ConfigParam instances, ConfigMacro
instances) tuple (as returned by get_config_data())
Keyword arguments:
fname - also write the content is to the file called "fname".
WARNING: if 'fname' names an existing file, it will be
overwritten!
"""
params, macros = config[0] or {}, config[1] or {}
Config._check_required_parameters(params)
params_with_values = [
p for p in params.values() if p.value is not None
]
ctx = {
"cfg_params": sorted([
(p.macro_name, str(p.value), p.set_by)
for p in params_with_values
]),
"macros": sorted([
(m.macro_name, str(m.macro_value or ""), m.defined_by)
for m in macros.values()
]),
"name_len": max([len(m.macro_name) for m in macros.values()] +
[len(m.macro_name) for m in params_with_values]
+ [0]),
"val_len": max([len(str(m.value)) for m in params_with_values] +
[len(m.macro_value or "") for m in macros.values()]
+ [0]),
}
jinja_loader = FileSystemLoader(dirname(abspath(__file__)))
jinja_environment = Environment(loader=jinja_loader,
undefined=StrictUndefined)
header_data = jinja_environment.get_template("header.tmpl").render(ctx)
# If fname is given, write "header_data" to it
if fname:
with open(fname, "w+") as file_desc:
file_desc.write(header_data)
return header_data
def get_config_data_header(self, fname=None):
""" Convert a Config instance to the content of a C header file, meant
to be included to a C/C++ file. The content is returned as a string.
Keyword arguments:
fname - also write the content to the file called "fname".
WARNING: if 'fname' names an existing file, it will be
overwritten!
"""
return self.config_to_header(self.get_config_data(), fname)