| # 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() |