Skip to content
Snippets Groups Projects
Commit c18fbeba authored by Nicola Sparnacci's avatar Nicola Sparnacci
Browse files

[CORE] Update features check for flavours


The function `local_conf_sanity_check` has been updated to consider also
the variables assigned with `:append` and `+=` in the `local.conf`.

The variables are checked over `*-feature.conf` files which are included
in the layers needing mandatory features.

The function shortly does the following:
- check if the variables in the `*-feature.conf` are in the `local.conf`

For each variable both in `local.conf` and `*-feature.conf`:
- if a variable is assigned in `local.conf` with a different assignment
  (i.e. `0` vs `+=`, `:append` vs `+=`, etc), return a configuration
  error.

- if a variable is assigned in `local.conf` with a different value
  than the one in the `*-feature.conf`, return a configuration error.

- if a variable is assigned in `local.conf`` & `*-feature.conf` with the
  same values and operators, the one in the `local.conf` are removed
  and the one in `*-feature.conf` is preserved.

- if a variable is assigned with `:append` or `+=` in the
  `local.conf`, the common values are removed from `local.conf` as they
  are already in `*-feature.conf`. For example:
    ```
    local.conf: VAR:append = " A B"
    *-feature.conf: VAR:append = " A"
    will result in:
    local.conf: VAR:append = " B"
    *-feature.conf: VAR:append = " A"
    ```
    the same applies for `+=` but it doesn't for `=`.

Signed-off-by: default avatarNicola Sparnacci <nicola.sparnacci@seco.com>
parent fcbbeaa4
No related branches found
No related tags found
No related merge requests found
Pipeline #276342 passed with warnings with stage
in 1 minute and 53 seconds
......@@ -255,6 +255,64 @@ def prepare_local_conf(build_dir):
ssu.eprint(f"Error while processing files: {e}")
sys.exit(1)
##
# @brief Update or remove BitBake variable assignments in a given text block.
#
# This function scans the input `text` for BitBake variable assignments and applies
# updates based on the entries in `modified_values`. It supports removal, single-line,
# and multi-line reformatting depending on the values provided.
#
# @param text The original text (e.g., contents of a .conf or .bb file) to search for variables.
# @param modified_values A dictionary where each key is a variable name, and the value is a dict
# with the keys:
# - "modifier": BitBake modifier (e.g., "append", "prepend")
# - "class_override": Optional class override (e.g., "pn-somepackage")
# - "operator": BitBake assignment operator (e.g., "=", "+=")
# - "value": A string of the new value(s), possibly multiline.
#
# @return A modified version of the text with relevant variable assignments updated or removed.
def update_bitbake_vars_in_text(text, modified_values):
def replacement(match):
var = match.group("var")
# Preserve original line
if var not in modified_values:
return match.group(0)
entry = modified_values[var]
new_values = entry.get("value", "").strip().split()
# Remove the variable entirely if the new value is empty
if len(new_values) == 0:
return ""
# Replace as single line if only one value
elif len(new_values) == 1:
formatted_val = new_values[0]
# Replace as multi-line if more than one value
elif len(new_values) > 1:
formatted_val = ' \\\n ' + ' \\\n '.join(new_values) + ' \\\n'
modifier_str = f":{entry['modifier']}" if entry.get("modifier") else ""
override_str = f":{entry['class_override']}" if entry.get("class_override") else ""
operator = entry.get("operator")
space = " " if modifier_str == ":append" else ""
output = f'{var}{modifier_str}{override_str} {operator} "{space}{formatted_val}"\n'
return output
pattern = re.compile(
r'^[ \t]*'
r'(?P<var>[A-Za-z0-9_]+)'
r'(:(?P<modifier>append|prepend|remove))?'
r'(:(?P<override>[A-Za-z0-9_-]+))?'
r'\s*(?P<op>\+?=|:=|\?=|=)\s*'
r'"(?P<value>(?:\\\n|[^"])*?)"'
r'[ \t]*\n?',
flags=re.MULTILINE
)
return pattern.sub(replacement, text)
##
# @brief Performs a sanity check on the `local.conf` file in a Yocto build
# directory.
......@@ -311,41 +369,93 @@ def local_conf_sanity_check(build_dir):
# Collect the variables in the local.conf
local_conf_vars_dic = extract_yocto_variables_from_file(local_conf_path)
# Check for values mismatch or duplicates. The first case triggers an error,
# the second case forces the configurator to remove the variable from
# local.conf
local_conf_vars_to_remove = []
for feature_var in complete_feature_dict:
if feature_var in local_conf_vars_dic:
if complete_feature_dict[feature_var] != local_conf_vars_dic[feature_var]:
logger.error(f"Feature mismatch: '{feature_var}' exists in both local.conf and flavour's features file "
f"with different values: '{local_conf_vars_dic[feature_var]}' (local.conf) vs. "
f"'{complete_feature_dict[feature_var]}' (features file).")
ssu.eprint(f"Feature mismatch: '{feature_var}' exists in both local.conf and flavour's features file "
f"with different values: '{local_conf_vars_dic[feature_var]}' (local.conf) vs. "
f"'{complete_feature_dict[feature_var]}' (features file).")
modified_values = {}
for var, feat_entry in complete_feature_dict.items():
local_entry = local_conf_vars_dic.get(var)
if not local_entry:
logger.error(f"Missing {var} in the configuration. Check the feature file for the list of required settings.")
ssu.eprint(f"Missing {var} in the configuration. Check the feature file for the list of required settings.")
sys.exit(1)
if feat_entry["modifier"] != local_entry["modifier"] or feat_entry["operator"] != local_entry["operator"]:
logger.error(f"Variable {var} has different modifier or operator in feature file and local.conf. Aborting.")
ssu.eprint(f"Variable {var} has different modifier or operator in feature file and local.conf. Aborting.")
sys.exit(1)
feat_values = feat_entry["value"].split()
local_values = local_entry["value"].split()
if local_entry["modifier"] == "append" or local_entry["operator"] == "+=":
missing_values = [v for v in feat_values if v not in local_values]
if missing_values:
logger.debug(f"Missing required features {missing_values} for variable {var}. Aborting.")
ssu.eprint(f"Missing required features {missing_values} for variable {var}. Aborting.")
sys.exit(1)
else:
local_conf_vars_to_remove.append(feature_var)
# Remove found variables from the local.conf (if any)
if local_conf_vars_to_remove:
with open(local_conf_path, "r") as file:
lines = file.readlines()
updated_lines = lines
for feature_var in local_conf_vars_to_remove:
assignment_pattern = re.compile(r'^\s*' + re.escape(feature_var) + r'\s*.=\s*(?:"(?:[^"\\]|\\.)*"|\'.*?\'|[^#]*?)(?:\\\s*\n\s*)?$')
updated_lines = [line for line in lines if not assignment_pattern.match(line)]
logger.warning(f"Feature variable: '{feature_var}' already exists with the same value in the flavour's features file.\n"
"Removing the variable from the local.conf.")
ssu.eprint(f"Feature variable: '{feature_var}' already exists with the same value in the flavour's features file.\n"
"Removing the variable from the local.conf.")
logger.debug(f"updated_lines: {updated_lines}.")
# Write the updated lines back to the file (overwriting it)
with open(local_conf_path, 'w') as file:
file.writelines(updated_lines)
else:
if feat_entry["value"] != local_entry["value"]:
logger.debug(f"Variable {var} values differ. Aborting.")
ssu.eprint(f"Variable {var} values differ. Aborting.")
sys.exit(1)
updated_values = [v for v in local_values if v not in feat_values]
updated_value_str = " ".join(updated_values)
modified_values[var] = {
"modifier": local_entry["modifier"],
"class_override": local_entry["class_override"],
"operator": local_entry["operator"],
"value": " ".join(updated_values)
}
if updated_values:
logger.debug(f"Updated {var} to: {updated_value_str}")
else:
logger.debug(f"Duplicate found for {var}. Removing it from local.conf.")
with open(local_conf_path) as f:
content = f.read()
new_content = update_bitbake_vars_in_text(content, modified_values)
logger.debug(f"Updated local.conf content:\n{new_content}.")
with open(local_conf_path, "w") as f:
f.write(new_content)
# # Check for values mismatch or duplicates. The first case triggers an error,
# # the second case forces the configurator to remove the variable from
# # local.conf
# local_conf_vars_to_remove = []
# for feature_var in complete_feature_dict:
# if feature_var in local_conf_vars_dic:
# if complete_feature_dict[feature_var] != local_conf_vars_dic[feature_var]:
# logger.error(f"Feature mismatch: '{feature_var}' exists in both local.conf and flavour's features file "
# f"with different values: '{local_conf_vars_dic[feature_var]}' (local.conf) vs. "
# f"'{complete_feature_dict[feature_var]}' (features file).")
# ssu.eprint(f"Feature mismatch: '{feature_var}' exists in both local.conf and flavour's features file "
# f"with different values: '{local_conf_vars_dic[feature_var]}' (local.conf) vs. "
# f"'{complete_feature_dict[feature_var]}' (features file).")
# sys.exit(1)
# else:
# local_conf_vars_to_remove.append(feature_var)
# # Remove found variables from the local.conf (if any)
# if local_conf_vars_to_remove:
# with open(local_conf_path, "r") as file:
# lines = file.readlines()
# updated_lines = lines
# for feature_var in local_conf_vars_to_remove:
# assignment_pattern = re.compile(r'^\s*' + re.escape(feature_var) + r'\s*.=\s*(?:"(?:[^"\\]|\\.)*"|\'.*?\'|[^#]*?)(?:\\\s*\n\s*)?$')
# updated_lines = [line for line in lines if not assignment_pattern.match(line)]
# logger.warning(f"Feature variable: '{feature_var}' already exists with the same value in the flavour's features file.\n"
# "Removing the variable from the local.conf.")
# ssu.eprint(f"Feature variable: '{feature_var}' already exists with the same value in the flavour's features file.\n"
# "Removing the variable from the local.conf.")
# logger.debug(f"updated_lines: {updated_lines}.")
# # Write the updated lines back to the file (overwriting it)
# with open(local_conf_path, 'w') as file:
# file.writelines(updated_lines)
##
# @brief Reorders sections in the `local.conf` file within a Yocto build directory.
......@@ -511,42 +621,54 @@ def collect_bblayers_conf_seco_layers(bblayers_conf_path):
return layers_relative_paths
##
# @brief Extracts Yocto variables from a given file.
#
# This function reads the entire content of the specified file, searches
# for Yocto variables defined with various assignment operators (e.g., '=',
# '+=', ':=', '?=', etc.), and extracts their names and assigned values.
# It handles multi-line values, removing newlines and escape characters,
# and ensures that variable values are properly formatted (with extra
# spaces removed).
#
# @param file_path the path to the file containing Yocto variables.
#
# @return dict: A dictionary where the keys are the variable names, and the
# values are their corresponding assigned values, cleaned of extra spaces
# and escape characters.
# @brief Extracts BitBake/Yocto-style variables from a given configuration file.
#
# Parses the input file to extract variable assignments following BitBake syntax,
# such as VAR = "value", VAR:append = "value", or VAR:append:pn-myrecipe = "value".
#
# @param file_path Path to the Yocto configuration (.conf) or recipe (.bb/.bbappend) file.
# @return dict A dictionary mapping variable names to their parsed components:
# - "modifier" (str or None): e.g., "append", "prepend", or "remove".
# - "class_override" (str or None): e.g., "pn-myrecipe" if present.
# - "operator" (str): Assignment operator such as '=', '+=', '?=', etc.
# - "value" (str): Normalized value with extra whitespace and line continuations removed.
#
# Example output:
# {
# "VAR": {
# "modifier": "append",
# "class_override": "pn-myrecipe",
# "operator": "=",
# "value": "foo bar"
# }
# }
#/
def extract_yocto_variables_from_file(file_path):
variables = {}
with open(file_path, 'r') as file:
content = file.read()
# Pattern to match all Yocto variable assignment (all possible symbols)
# pattern = r'([A-Za-z0-9_]+)\s*(=?\+?=|:=|\?=|\??=|\.=|=\.|=)\s*"([^"]*)"'
pattern = r'^\s*([^#][A-Za-z0-9_]+)\s*(=?\+?=|:=|\?=|\??=|\.=|=\.|=)\s*"([^"]*)"'
# Find all matches for the variables in the content
matches = re.findall(pattern, content, flags=re.DOTALL | re.MULTILINE)
# Store each variable in the dictionary
for var_name, _, var_value in matches:
value = var_value.replace('\n', ' ').replace('\\', ' ').strip()
variables[var_name] = " ".join(value.split())
logger.debug(f"Extracted from {file_path} the Yocto variables:\n{variables}.")
pattern = re.compile(
r'^\s*'
r'(?P<var>[A-Za-z0-9_]+)'
r'(:(?P<modifier>append|prepend|remove))?'
r'(:(?P<class_override>[A-Za-z0-9_-]+))?'
r'\s*(?P<op>\+?=|:=|\?=|=)\s*'
r'"(?P<value>[^"]*)"',
flags=re.MULTILINE
)
for match in pattern.finditer(content):
var_name = match.group("var")
variables[var_name] = {
"modifier": match.group("modifier"),
"class_override": match.group("class_override"),
"operator": match.group("op"),
"value": " ".join(match.group("value").replace('\n', '').replace('\\', '').strip().split())
}
logger.debug(f"Extracted variables from {file_path} the variables:\n{variables}")
return variables
##
......
......@@ -417,15 +417,15 @@ def environment_manager(args_cwd, args_build_dir, args_sdkmachine, reconfigure,
# Generate SRCREV.conf starting from the bblayers.conf
ssc.generate_srcrev_conf(build_dir, custom_layer)
# local.conf sanity check against layers' feature files
ssc.local_conf_sanity_check(build_dir)
# local.conf normalize structure (reorder sections)
ssc.local_conf_normalize_structure(build_dir)
# Merge multiple variable assignment into a single assignment
ssc.local_conf_merge_variables(build_dir)
# local.conf sanity check against layers' feature files
ssc.local_conf_sanity_check(build_dir)
else:
new_build_created = False
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment