From 486dc991530d91ab4046f573232f7db8052353b3 Mon Sep 17 00:00:00 2001 From: yan hu Date: Sat, 4 Jul 2026 17:50:27 +0800 Subject: [PATCH] tools: harden attachconfig handling --- tools/attachconfig.py | 5 +- tools/ci/bsp_buildings.py | 112 +++++++------ .../testcases/test_attachconfig_robustness.py | 156 ++++++++++++++++++ 3 files changed, 226 insertions(+), 47 deletions(-) create mode 100644 tools/testcases/test_attachconfig_robustness.py diff --git a/tools/attachconfig.py b/tools/attachconfig.py index df89fd23c3d..5d52c4e8c1a 100644 --- a/tools/attachconfig.py +++ b/tools/attachconfig.py @@ -16,6 +16,9 @@ def GenAttachConfigProject(program = None): attachconfig=[] GetAttachConfig("get",attachconfig,0) print("\033[32m✅ AttachConfig has: \033[0m") + if attachconfig==[]: + print("No AttachConfig found.") + return prefix=attachconfig[0] for line in attachconfig: temp_prefix=line.split(".", 1) @@ -84,4 +87,4 @@ def GetAttachConfig(action,attachconfig,attachconfig_result): from ci.bsp_buildings import get_details_and_dependencies detail_list=get_details_and_dependencies([name],projects) for line in detail_list: - attachconfig_result.append(line) \ No newline at end of file + attachconfig_result.append(line) diff --git a/tools/ci/bsp_buildings.py b/tools/ci/bsp_buildings.py index a07340d4397..3808b6f11a2 100644 --- a/tools/ci/bsp_buildings.py +++ b/tools/ci/bsp_buildings.py @@ -115,6 +115,30 @@ def is_env_enabled(name, default=False): return value.lower() not in ('', '0', 'false', 'no', 'off') +def parse_csv_env(name, required=False): + value = os.getenv(name) + if value is None or value.strip() == '': + if required: + print(f"::error::{name} is not set or empty") + sys.exit(1) + return None + + items = [item.strip() for item in value.split(',') if item.strip()] + if required and items == []: + print(f"::error::{name} is not set or empty") + sys.exit(1) + + return items + +def split_config_commands(value): + if value is None: + return None + if isinstance(value, str): + return value.splitlines() + if isinstance(value, (list, tuple)): + return list(value) + return [value] + def run_dist_build_check(bsp, scons_args=''): """ build BSP distribution and verify that the generated project can compile. @@ -381,15 +405,10 @@ def check_output(output, check_string): qemu_timeout_second = 50 rtt_root = os.getcwd() - srtt_bsp = os.getenv('SRTT_BSP').split(',') - attachconfig_rtt_bsp_env = os.getenv('ATTACHCONFIG_RTT_BSP') - attachconfig_rtt_bsp = None - if attachconfig_rtt_bsp_env: - attachconfig_rtt_bsp = { - bsp.strip() - for bsp in attachconfig_rtt_bsp_env.split(',') - if bsp.strip() - } + srtt_bsp = parse_csv_env('SRTT_BSP', required=True) + attachconfig_rtt_bsp = parse_csv_env('ATTACHCONFIG_RTT_BSP') + if attachconfig_rtt_bsp is not None: + attachconfig_rtt_bsp = set(attachconfig_rtt_bsp) print(srtt_bsp) for bsp in srtt_bsp: count += 1 @@ -444,9 +463,9 @@ def check_output(output, check_string): # 如果是bsp_board_info,读取基本的信息 if(name == 'bsp_board_info'): print(details) - pre_build_commands = details.get("pre_build").splitlines() - build_command = details.get("build_cmd").splitlines() - post_build_command = details.get("post_build").splitlines() + pre_build_commands = split_config_commands(details.get("pre_build")) + build_command = split_config_commands(details.get("build_cmd")) + post_build_command = split_config_commands(details.get("post_build")) qemu_command = details.get("run_cmd") if details.get("kconfig") is not None: @@ -455,7 +474,7 @@ def check_output(output, check_string): else: build_check_result = None if details.get("msh_cmd") is not None: - commands = details.get("msh_cmd").splitlines() + commands = split_config_commands(details.get("msh_cmd")) else: msh_cmd = None if details.get("checkresult") is not None: @@ -467,54 +486,55 @@ def check_output(output, check_string): else: ci_build_run_flag = None if details.get("pre_build") is not None: - pre_build_commands = details.get("pre_build").splitlines() + pre_build_commands = split_config_commands(details.get("pre_build")) if details.get("env") is not None: bsp_build_env = details.get("env") else: bsp_build_env = None if details.get("build_cmd") is not None: - build_command = details.get("build_cmd").splitlines() + build_command = split_config_commands(details.get("build_cmd")) else: build_command = None if details.get("post_build") is not None: - post_build_command = details.get("post_build").splitlines() + post_build_command = split_config_commands(details.get("post_build")) if details.get("run_cmd") is not None: qemu_command = details.get("run_cmd") else: qemu_command = None + if details.get("kconfig") is None: + continue count += 1 config_bacakup = config_file+'.origin' shutil.copyfile(config_file, config_bacakup) - #加载yml中的配置放到.config文件 - with open(config_file, 'a') as destination: - if details.get("kconfig") is None: - #如果没有Kconfig,读取下一个配置 - continue - if(projects.get(name) is not None): - # 获取Kconfig中所有的信息 - detail_list=get_details_and_dependencies([name],projects) - for line in detail_list: - destination.write(line + '\n') - scons_arg=[] - #如果配置中有scons_arg - if details.get('scons_arg') is not None: - for line in details.get('scons_arg'): - scons_arg.append(line) - scons_arg_str=' '.join(scons_arg) if scons_arg else ' ' - print(f"::group::\tCompiling yml project: =={count}==={name}=scons_arg={scons_arg_str}==") - # #开始编译bsp - res = build_bsp(bsp, scons_arg_str,name=name,pre_build_commands=pre_build_commands,post_build_command=post_build_command,build_check_result=build_check_result,bsp_build_env =bsp_build_env) - if not res: - print(f"::error::build {bsp} {name} failed.") - add_summary(f'\t- ❌ build {bsp} {name} failed.') - failed += 1 - else: - add_summary(f'\t- ✅ build {bsp} {name} success.') - print("::endgroup::") - - - shutil.copyfile(config_bacakup, config_file) - os.remove(config_bacakup) + try: + #加载yml中的配置放到.config文件 + with open(config_file, 'a') as destination: + if(projects.get(name) is not None): + # 获取Kconfig中所有的信息 + detail_list=get_details_and_dependencies([name],projects) + for line in detail_list: + destination.write(line + '\n') + scons_arg=[] + #如果配置中有scons_arg + if details.get('scons_arg') is not None: + for line in details.get('scons_arg'): + scons_arg.append(line) + scons_arg_str=' '.join(scons_arg) if scons_arg else ' ' + print(f"::group::\tCompiling yml project: =={count}==={name}=scons_arg={scons_arg_str}==") + # #开始编译bsp + res = build_bsp(bsp, scons_arg_str,name=name,pre_build_commands=pre_build_commands,post_build_command=post_build_command,build_check_result=build_check_result,bsp_build_env =bsp_build_env) + if not res: + print(f"::error::build {bsp} {name} failed.") + add_summary(f'\t- ❌ build {bsp} {name} failed.') + failed += 1 + else: + add_summary(f'\t- ✅ build {bsp} {name} success.') + print("::endgroup::") + + + finally: + shutil.copyfile(config_bacakup, config_file) + os.remove(config_bacakup) diff --git a/tools/testcases/test_attachconfig_robustness.py b/tools/testcases/test_attachconfig_robustness.py new file mode 100644 index 00000000000..1d5ceb353c2 --- /dev/null +++ b/tools/testcases/test_attachconfig_robustness.py @@ -0,0 +1,156 @@ +import contextlib +import importlib.util +import os +import shutil +import subprocess +import sys +import tempfile +import types +import unittest +from io import StringIO + + +RTT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + + +def load_attachconfig_module(): + scons_mod = types.ModuleType("SCons") + script_mod = types.ModuleType("SCons.Script") + script_mod.GetOption = lambda name: None + + old_scons = sys.modules.get("SCons") + old_script = sys.modules.get("SCons.Script") + sys.modules["SCons"] = scons_mod + sys.modules["SCons.Script"] = script_mod + + try: + path = os.path.join(RTT_ROOT, "tools", "attachconfig.py") + spec = importlib.util.spec_from_file_location("attachconfig_under_test", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + finally: + if old_scons is None: + sys.modules.pop("SCons", None) + else: + sys.modules["SCons"] = old_scons + if old_script is None: + sys.modules.pop("SCons.Script", None) + else: + sys.modules["SCons.Script"] = old_script + + +class AttachConfigRobustnessTest(unittest.TestCase): + + def test_attach_query_handles_empty_attachconfig_list(self): + module = load_attachconfig_module() + module.GetOption = lambda name: "?" if name == "attach" else None + module.GetAttachConfig = lambda action, attachconfig, result: None + + output = StringIO() + with contextlib.redirect_stdout(output): + module.GenAttachConfigProject() + + self.assertIn("AttachConfig", output.getvalue()) + + def test_bsp_buildings_reports_missing_srtt_bsp_without_traceback(self): + env = os.environ.copy() + env.pop("SRTT_BSP", None) + + result = subprocess.run( + [sys.executable, os.path.join("tools", "ci", "bsp_buildings.py")], + cwd=RTT_ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + combined_output = result.stdout + result.stderr + self.assertNotEqual(result.returncode, 0) + self.assertIn("SRTT_BSP", combined_output) + self.assertNotIn("Traceback", combined_output) + def test_bsp_board_info_allows_missing_optional_commands(self): + bsp_name = "tmp_attachconfig_board_info_test" + bsp_dir = os.path.join(RTT_ROOT, "bsp", bsp_name) + attach_dir = os.path.join(bsp_dir, ".ci", "attachconfig") + + shutil.rmtree(bsp_dir, ignore_errors=True) + os.makedirs(attach_dir, exist_ok=True) + + try: + config_file = os.path.join(bsp_dir, ".config") + backup_file = config_file + ".origin" + with open(config_file, "w", encoding="utf-8") as f: + f.write("CONFIG_BASE=y\n") + + with open(os.path.join(attach_dir, "board_attachconfig.yml"), "w", encoding="utf-8") as f: + f.write( + "bsp_board_info:\n" + " run_cmd: \"echo run\"\n" + "board_project:\n" + " kconfig:\n" + " - CONFIG_BOARD_PROJECT=y\n" + ) + + env = os.environ.copy() + env["SRTT_BSP"] = bsp_name + + result = subprocess.run( + [sys.executable, os.path.join("tools", "ci", "bsp_buildings.py")], + cwd=RTT_ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + combined_output = result.stdout + result.stderr + self.assertNotIn("AttributeError", combined_output) + self.assertNotIn("Traceback", combined_output) + self.assertFalse(os.path.exists(backup_file)) + finally: + shutil.rmtree(bsp_dir, ignore_errors=True) + def test_attachconfig_without_kconfig_does_not_leave_backup(self): + bsp_name = "tmp_attachconfig_test" + bsp_dir = os.path.join(RTT_ROOT, "bsp", bsp_name) + attach_dir = os.path.join(bsp_dir, ".ci", "attachconfig") + + shutil.rmtree(bsp_dir, ignore_errors=True) + os.makedirs(attach_dir, exist_ok=True) + + try: + config_file = os.path.join(bsp_dir, ".config") + backup_file = config_file + ".origin" + with open(config_file, "w", encoding="utf-8") as f: + f.write("CONFIG_BASE=y\n") + + with open(os.path.join(attach_dir, "bad_attachconfig.yml"), "w", encoding="utf-8") as f: + f.write( + "no_kconfig_project:\n" + " pre_build: \"echo pre\"\n" + " build_cmd: \"echo build\"\n" + " post_build: \"echo post\"\n" + ) + + env = os.environ.copy() + env["SRTT_BSP"] = bsp_name + + subprocess.run( + [sys.executable, os.path.join("tools", "ci", "bsp_buildings.py")], + cwd=RTT_ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + self.assertFalse(os.path.exists(backup_file)) + with open(config_file, "r", encoding="utf-8") as f: + self.assertEqual(f.read(), "CONFIG_BASE=y\n") + finally: + shutil.rmtree(bsp_dir, ignore_errors=True) + + +if __name__ == "__main__": + unittest.main()