#!/usr/bin/env python3 from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from hashlib import md5 import json import logging import os import requests HEADER_FILE_TEMPLATE = """{license_header} {generated_warning} #pragma once #include "common/streambuf.h" {defines} void sbufWriteBuildInfoFlags(sbuf_t *dst); """ SOURCE_FILE_TEMPLATE = """{license_header} {generated_warning} #include #include "platform.h" #include "common/streambuf.h" #include "msp/msp_build_info.h" void sbufWriteBuildInfoFlags(sbuf_t *dst) { static const uint16_t options[] = { {build_options} }; for (unsigned i = 0; i < ARRAYLEN(options); i++) { sbufWriteU16(dst, options[i]); } } """ WARNING_COMMENT_TEMPLATE = """ /* * WARNING: This is an auto-generated file, please do not edit directly! * * Generator : `src/utils/make-build-info.py` * Source : {source} * Input hash : {input_hash} */ """ def __find_project_root() -> str: utils_dir = os.path.abspath(os.path.dirname(__file__)) src_dir = os.path.dirname(utils_dir) root_dir = os.path.dirname(src_dir) return os.path.realpath(root_dir) def camel_case_to_title(s: str) -> str: if not s: return "Unspecified" else: spaceless = s.replace(" ", "") return "".join([(c if not c.isupper() else f" {c}") for c in spaceless]) \ .lstrip() \ .title() def fetch_build_options(endpoint_url: str) -> tuple: logging.info(f"Fetching JSON: {endpoint_url}") data = requests.get(endpoint_url, timeout=2).json() input_hash = md5(json.dumps(data, sort_keys=True).encode()).hexdigest() logging.info(f"Input hash: {input_hash}") defines = [] options = [] groups = list(data.keys()) for group_index, option_list in enumerate(data.values()): for option in option_list: define = option.get("value") if define: defines.append(define) number = option.get("key") name = define.replace("USE_", "BUILD_OPTION_") options.append((name, number, camel_case_to_title(groups[group_index]))) logging.info(f"Number of defines: {len(defines)}") return defines, options, input_hash def get_warning_comment(source: str, input_hash: str) -> str: return WARNING_COMMENT_TEMPLATE \ .strip() \ .replace("{source}", source) \ .replace("{input_hash}", input_hash) \ def main(root_path: str, target_path: str, endpoint_url: str): logging.info(f"Project root: {root_path}") license_file_path = os.path.join(root_path, "DEFAULT_LICENSE.md") msp_build_info_c_path = os.path.join(target_path, "msp_build_info.c") msp_build_info_h_path = os.path.join(target_path, "msp_build_info.h") with open(license_file_path) as f: license_header = f.read().rstrip() gates, options, input_hash = fetch_build_options(endpoint_url) generated_warning = get_warning_comment(endpoint_url, input_hash) with open(msp_build_info_h_path, "w+") as f: max_len = max(map(lambda x: len(x[0]), options)) + 4 lines = [] last_group = None for option_name, option_value, group in options: if group != last_group: lines.append(f"// {group}") last_group = group lines.append(f"#define {option_name:<{max_len}}{option_value}") data = HEADER_FILE_TEMPLATE \ .replace("{license_header}", license_header) \ .replace("{generated_warning}", generated_warning) \ .replace("{defines}", "\n".join(lines)) f.write(data) logging.info(f"Written header file: {msp_build_info_h_path}") with open(msp_build_info_c_path, "w+") as f: lines = [] indent = " " * 8 for i, define in enumerate(gates): option_name, _, _ = options[i] lines.append(f"#ifdef {define}") lines.append(f"{indent}{option_name},") lines.append("#endif") data = SOURCE_FILE_TEMPLATE \ .replace("{license_header}", license_header) \ .replace("{generated_warning}", generated_warning) \ .replace("{build_options}", "\n".join(lines)) f.write(data) logging.info(f"Written source file: {msp_build_info_c_path}") if __name__ == "__main__": PROJECT_ROOT_DIR = __find_project_root() DEFAULT_TARGET_DIR = os.path.join(PROJECT_ROOT_DIR, "src", "main", "msp") parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument("endpoint_url", help="URL to build options API endpoint") parser.add_argument("-d", "--target-dir", default=DEFAULT_TARGET_DIR, help="Path to output directory") parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() logging.basicConfig(level=logging.INFO if args.verbose else logging.ERROR) main( root_path=PROJECT_ROOT_DIR, target_path=args.target_dir, endpoint_url=args.endpoint_url, )