import json
import os
import re
import shutil
import sys
from collections import OrderedDict
from pathlib import Path
def parse_config_line(line):
"""
Parse a configuration line and return the key and value.
Args:
line (str): A line from a configuration file
Returns:
tuple: (key, value) if the line contains a configuration, (None, None) otherwise
Handles two formats:
1. "# CONFIG_XXX is not set" -> returns (CONFIG_XXX, 'n')
2. "CONFIG_XXX=value" -> returns (CONFIG_XXX, value)
"""
line = line.strip()
if not line:
return None, None
if line.startswith("# ") and line.endswith(" is not set"):
config_name = line.split()[1]
return config_name, "n"
if "=" in line:
key, value = line.split("=", 1)
return key, value
return None, None
def opposite(value):
if value == "n":
return "y"
else:
return "n"
def expand_file(input_path, include_paths, processed=None, tree_node=None):
"""
Recursively expand a configuration file with #include directives.
Args:
input_path (str): Path to the input configuration file
include_paths (list): List of directories to search for included files
processed (set, optional): Set of already processed files to avoid circular includes
tree_node (dict, optional): Node in the configuration tree being built
Returns:
tuple: (list of expanded lines, tree structure node)
This function:
1. Reads the input file line by line
2. Processes #include directives by recursively expanding included files
3. Parses configuration lines to build a configuration dictionary
4. Builds a tree structure representing the file inclusion hierarchy
5. Returns the expanded content and the tree structure
"""
if processed is None:
processed = set()
if tree_node is None:
tree_node = {
"file": str(input_path),
"includes": [],
"configs": OrderedDict(),
"include_lines": [],
"raw_content": [],
}
input_path = Path(input_path).resolve()
if input_path in processed:
return [], tree_node
processed.add(input_path)
expanded_lines = []
current_configs = OrderedDict()
with input_path.open("r", encoding="utf-8") as f:
lines = f.readlines()
tree_node["raw_content"] = [line.rstrip("\n") for line in lines]
for line in lines:
line_strip = line.strip()
match = re.match(r"#include\s*[<\"]([^\">]+)[\">]", line_strip)
if match:
include_file = match.group(1)
found = False
tree_node["include_lines"].append(line.rstrip("\n"))
direct_path = input_path.parent / include_file
if direct_path.exists():
include_node = {
"file": str(direct_path),
"includes": [],
"configs": OrderedDict(),
"include_lines": [],
"raw_content": [],
}
tree_node["includes"].append(include_node)
included_lines, include_node = expand_file(
direct_path, include_paths, processed, include_node
)
expanded_lines.extend(included_lines)
for key, value in include_node["configs"].items():
current_configs[key] = value
tree_node["configs"][key] = value
found = True
else:
for path in include_paths:
candidate = Path(path) / include_file
if candidate.exists():
include_node = {
"file": str(candidate),
"includes": [],
"configs": OrderedDict(),
"include_lines": [],
"raw_content": [],
}
tree_node["includes"].append(include_node)
included_lines, include_node = expand_file(
candidate, include_paths, processed, include_node
)
expanded_lines.extend(included_lines)
for key, value in include_node["configs"].items():
current_configs[key] = value
tree_node["configs"][key] = value
found = True
break
if not found:
print(
f'ERROR: Cannot find "{include_file}" from {input_path}',
file=sys.stderr,
)
sys.exit(1)
else:
key, value = parse_config_line(line)
if key is not None:
current_configs[key] = value
tree_node["configs"][key] = value
expanded_lines.append(line)
expanded_lines.append("\n")
return expanded_lines, tree_node
def preprocess(output_path, input_path, include_paths, tree_output_path=None):
"""
Process a configuration file with #include directives.
Args:
output_path (str): Path to write the expanded configuration
input_path (str): Path to the input configuration file
include_paths (list): List of directories to search for included files
tree_output_path (str, optional): Path to write the tree structure
This function:
1. Expands the input file by processing #include directives
2. Writes the expanded configuration to output_path
3. Optionally writes the tree structure to tree_output_path for postprocessing
"""
lines, tree = expand_file(input_path, include_paths)
with open(output_path, "w", encoding="utf-8") as out:
out.writelines(lines)
if tree_output_path and tree["includes"]:
with open(tree_output_path, "w", encoding="utf-8") as f:
json.dump(tree, f, indent=2, ensure_ascii=False)
def get_all_included_configs(tree):
"""
Extract all configuration options from included files.
Args:
tree (dict): The configuration tree structure
Returns:
OrderedDict: Dictionary of configuration options from all included files
This function recursively traverses the tree to collect all configurations
from files included via #include directives.
"""
included_configs = OrderedDict()
def collect_configs(node):
for include in node.get("includes", []):
collect_configs(include)
for key, value in node.get("configs", {}).items():
included_configs[key] = value
for include in tree.get("includes", []):
collect_configs(include)
return included_configs
def get_main_configs(tree):
"""
Extract configuration options from the main file (excluding #include directives).
Args:
tree (dict): The configuration tree structure
Returns:
OrderedDict: Dictionary of configuration options from the main file
"""
main_configs = OrderedDict()
for line in tree["raw_content"]:
key, value = parse_config_line(line)
if key is not None:
main_configs[key] = value
return main_configs
def get_current_configs(config_path):
"""
Parse the current full configuration file.
Args:
config_path (str): Path to the current configuration file
Returns:
OrderedDict: Dictionary of configuration options from the current file
"""
configs = OrderedDict()
with open(config_path, "r", encoding="utf-8") as f:
for line in f:
key, value = parse_config_line(line)
if key is not None:
configs[key] = value
return configs
def postprocess_inner(tree_path, added, changed, removed, output_path):
"""
Postprocess configuration changes to generate a defconfig with #include directives.
This function takes the specific changes (added, changed, removed) calculated
by postprocess and applies them to the original defconfig structure
represented by the tree, producing a new defconfig file.
Args:
tree_path (str): Path to the config_tree.json generated during preprocessing of the ORIGINAL defconfig.
added (dict): {key: value} - Configurations added by the user.
changed (dict): {key: (old_value, new_value)} - Configurations changed by the user.
removed (dict): {key: old_value} - Configurations removed by the user.
output_path (str): Path where the new defconfig should be written.
"""
with open(tree_path, "r", encoding="utf-8") as f:
original_tree = json.load(f, object_pairs_hook=OrderedDict)
original_included_configs = get_all_included_configs(original_tree)
original_main_configs = get_main_configs(original_tree)
final_main_configs = OrderedDict()
for key in original_included_configs:
if key in added:
final_main_configs[key] = added[key]
elif key in changed:
final_main_configs[key] = changed[key][1]
elif key in removed:
final_main_configs[key] = opposite(removed[key])
for key in original_main_configs:
if key in added:
final_main_configs[key] = added[key]
elif key in changed:
final_main_configs[key] = changed[key][1]
elif key in removed:
final_main_configs[key] = opposite(removed[key])
else:
final_main_configs[key] = original_main_configs[key]
for key, value in added.items():
if key not in original_main_configs and key not in original_included_configs:
final_main_configs[key] = value
with open(output_path, "w", encoding="utf-8") as f:
for include_line in original_tree.get("include_lines", []):
f.write(include_line + "\n")
if original_tree.get("include_lines"):
f.write("\n")
final_write_list = []
for key, value in final_main_configs.items():
if value == "n":
final_write_list.append(f"# {key} is not set\n")
else:
final_write_list.append(f"{key}={value}\n")
final_write_list.sort()
for write_line in final_write_list:
f.write(write_line)
def get_config_diff(old_config, new_config):
"""
Compare two config dictionaries and return the differences.
Args:
old_config (dict): The original configuration.
new_config (dict): The modified configuration.
Returns:
tuple: (added, changed, removed)
added (dict): Items in new_config but not in old_config.
changed (dict): Items with different values. {key: (old_value, new_value)}.
removed (dict): Items in old_config but not in new_config.
"""
added = {}
changed = {}
removed = {}
for key, new_value in new_config.items():
if key not in old_config:
added[key] = new_value
elif old_config[key] != new_value:
changed[key] = (old_config[key], new_value)
for key in old_config:
if key not in new_config:
removed[key] = old_config[key]
return added, changed, removed
def load_config_file(filepath):
"""
Load a .config or defconfig file into an OrderedDict.
"""
config = OrderedDict()
try:
with open(filepath, "r") as f:
for line in f:
key, value = parse_config_line(line)
if key is not None:
config[key] = value
except FileNotFoundError:
print(
f"Warning: Config file {filepath} not found. Treating as empty.",
file=sys.stderr,
)
return config
def postprocess(
tree_path, original_defconfig_path, modified_defconfig_path, output_defconfig_path
):
"""
An improved postprocess that compares defconfig files before and after modification.
This function addresses the issue where Kconfig's savedefconfig omits default values,
making it hard to distinguish user deletions from optimizations.
Args:
tree_path (str): Path to the config_tree.json generated during preprocessing.
original_defconfig_path (str): Path to the defconfig file BEFORE user modification.
modified_defconfig_path (str): Path to the defconfig file AFTER user modification.
output_defconfig_path (str): Path where the updated defconfig should be written.
"""
defconfig_original = load_config_file(original_defconfig_path)
defconfig_modified = load_config_file(modified_defconfig_path)
added, changed, removed = get_config_diff(defconfig_original, defconfig_modified)
postprocess_inner(tree_path, added, changed, removed, output_defconfig_path)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(
"Usage: process_config.py <mode> [options]",
file=sys.stderr,
)
print("Modes:", file=sys.stderr)
print(
" preprocess <output_file> <input_file> [include_paths...] [--tree <tree_file>]",
file=sys.stderr,
)
print(
" postprocess <tree_file> <original_defconfig> <modified_defconfig> <output_defconfig>",
file=sys.stderr,
)
sys.exit(1)
mode = sys.argv[1]
if mode == "preprocess":
if len(sys.argv) < 4:
print(
"Usage: preprocess <output_file> <input_file> [include_paths...] [--tree <tree_file>]",
file=sys.stderr,
)
sys.exit(1)
output_file = Path(sys.argv[2])
input_file = sys.argv[3]
include_dirs = []
tree_file = None
i = 4
while i < len(sys.argv):
if sys.argv[i] == "--tree" and i + 1 < len(sys.argv):
tree_file = sys.argv[i + 1]
i += 2
else:
include_dirs.append(sys.argv[i])
i += 1
if output_file.exists():
output_file.unlink()
preprocess(output_file, input_file, include_dirs, tree_file)
elif mode == "postprocess":
if len(sys.argv) < 6:
print(
"Usage: postprocess <tree_file> <original_defconfig> <modified_defconfig> <output_defconfig>",
file=sys.stderr,
)
sys.exit(1)
tree_file = sys.argv[2]
original_defconfig = sys.argv[3]
modified_defconfig = sys.argv[4]
output_defconfig = sys.argv[5]
if Path(tree_file).is_file():
post_defconfig = output_defconfig + "tmp"
postprocess(
tree_file, original_defconfig, modified_defconfig, post_defconfig
)
shutil.copy2(post_defconfig, output_defconfig)
os.remove(post_defconfig)
else:
shutil.copy2(modified_defconfig, output_defconfig)
else:
print(f"Unknown mode: {mode}", file=sys.stderr)
sys.exit(1)