blob: e4f1427f6587f9ef7f9e73342864c17ee5715972 [file] [edit]
# Copyright 2026 The Pigweed Authors
#
# 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
#
# https://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.
#!/usr/bin/env python3
import sys
import os
import argparse
import json
import glob
import hashlib
import re
import collections
try:
import yaml
except ImportError:
print("PyYAML is required but not found.", file=sys.stderr)
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--apps-json", required=True)
return parser.parse_args()
def parse_sysbuild_yaml(file_path):
try:
with open(file_path, 'r') as f:
return yaml.safe_load(f)
except Exception as e:
print(f"Error parsing {file_path}: {e}", file=sys.stderr)
return None
def process_sysbuild_conf(file_path):
fragments = collections.defaultdict(list)
try:
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
# Match global SB_CONFIG_ settings FIRST
match = re.match(r"^SB_CONFIG_([a-zA-Z0-9_]+)=(.*)$", line)
if match:
option = match.group(1)
value = match.group(2)
# Global SB_CONFIG_ options are generally for Sysbuild itself and
# ignored, except for specific settings that have hardcoded side-effects
# on the Kconfig of target images (like enabling MCUboot or its mode).
if option == "BOOTLOADER_MCUBOOT" and value == "y":
fragments["main"].append("CONFIG_BOOTLOADER_MCUBOOT=y")
fragments["mcuboot"].append("CONFIG_BOOTLOADER_MCUBOOT=y")
elif option.startswith("MCUBOOT_MODE_"):
translated_option = option.replace("MCUBOOT_MODE_", "CONFIG_MCUBOOT_BOOTLOADER_MODE_")
fragments["mcuboot"].append(f"{translated_option}={value}")
continue
# Match namespaced settings: domain_CONFIG_OPTION=value
match = re.match(r"^([a-zA-Z0-9_]+)_(CONFIG_[a-zA-Z0-9_]+)=(.*)$", line)
if match:
domain = match.group(1)
setting = f"{match.group(2)}={match.group(3)}"
fragments[domain].append(setting)
continue
except Exception as e:
print(f"Error reading {file_path}: {e}", file=sys.stderr)
return {domain: "\n".join(settings) + "\n" for domain, settings in fragments.items()}
def get_app_hash(app_pkg):
norm_app = app_pkg.lstrip("@").lstrip("/").lstrip(":")
if ":" in norm_app:
pkg, target = norm_app.split(":", 1)
if pkg.endswith(target) or target == pkg.split("/")[-1]:
norm_app = pkg
h = hashlib.md5(norm_app.encode('utf-8')).hexdigest()[:8]
return h
def sanitize_board_id(board_id):
return board_id.replace("/", "_").replace("-", "_").replace(".", "_")
def main():
args = parse_args()
apps = json.loads(args.apps_json)
raw_metadata = {}
translated_confs = {}
for app_pkg, app_path in apps.items():
app_graph = {"helpers": {}, "board_helpers": {}}
global_file = os.path.join(app_path, "sysbuild.yml")
if os.path.exists(global_file):
content = parse_sysbuild_yaml(global_file)
if content and "helpers" in content:
app_graph["helpers"] = content["helpers"]
boards_dir = os.path.join(app_path, "boards")
if os.path.exists(boards_dir):
pattern = os.path.join(boards_dir, "*.sysbuild.yml")
for board_file in glob.glob(pattern):
filename = os.path.basename(board_file)
board_name = filename.replace(".sysbuild.yml", "")
content = parse_sysbuild_yaml(board_file)
if content and "helpers" in content:
app_graph["board_helpers"][board_name] = content["helpers"]
if app_graph["helpers"] or app_graph["board_helpers"]:
raw_metadata[app_pkg] = app_graph
sysbuild_conf = os.path.join(app_path, "sysbuild.conf")
if os.path.exists(sysbuild_conf):
app_fragments = process_sysbuild_conf(sysbuild_conf)
if app_fragments:
translated_confs[app_pkg] = app_fragments
repos_to_declare = []
for app_pkg, app_path in apps.items():
if app_pkg not in raw_metadata:
continue
app_graph = raw_metadata[app_pkg]
app_fragments = translated_confs.get(app_pkg, {})
boards = set(app_graph["board_helpers"].keys())
if not boards:
boards.add("default_board")
for board_id in boards:
helpers = {}
helpers.update(app_graph["helpers"])
helpers.update(app_graph["board_helpers"].get(board_id, {}))
for helper_name, helper_config in helpers.items():
helper_target = helper_config.get("target")
if not helper_target:
continue
if helper_config.get("type") == "external":
continue
platform = helper_config.get("platform")
if platform:
# Use helper's platform as board_id for repo naming
helper_board_id = platform.split(":")[-1] if ":" in platform else os.path.basename(platform)
else:
helper_board_id = board_id
rel_conf_fragment_path = os.path.join("sysbuild", f"{helper_name}.conf")
conf_fragment_path = os.path.join(app_path, rel_conf_fragment_path)
has_conf_fragment = os.path.exists(conf_fragment_path)
helper_fragment = app_fragments.get(helper_name, "")
if has_conf_fragment or helper_fragment:
path_str = f"{app_pkg}->{helper_name}"
path_hash = hashlib.md5(path_str.encode('utf-8')).hexdigest()[:8]
safe_board = sanitize_board_id(helper_board_id)
repo_name = f"zc_{path_hash}_{safe_board}"
conf_fragments = []
if has_conf_fragment:
conf_fragments.append(rel_conf_fragment_path)
repos_to_declare.append({
"name": repo_name,
"app_label": helper_target,
"board_id": helper_board_id, # Use helper's board ID
"conf_fragments": conf_fragments,
"extra_kconfig": helper_fragment,
"parent_app": app_pkg,
"helper_name": helper_name
})
results = {
"raw_metadata": raw_metadata,
"custom_repos": repos_to_declare,
"translated_confs": translated_confs
}
print(json.dumps(results))
if __name__ == "__main__":
main()