1#
2# Copyright (c) 2025, RT-Thread Development Team
3#
4# SPDX-License-Identifier: Apache-2.0
5#
6# Change Logs:
7# Date           Author       Notes
8# 2025-04-21     supperthomas add the smart yml support and add env
9#
10import subprocess
11import threading
12import time
13import logging
14import sys
15import os
16import shutil
17import re
18import multiprocessing
19import yaml
20
21def add_summary(text):
22    """
23    add summary to github action.
24    """
25    os.system(f'echo "{text}" >> $GITHUB_STEP_SUMMARY ;')
26
27
28def run_cmd(cmd, output_info=True):
29    """
30    run command and return output and result.
31    """
32    print('\033[1;32m' + cmd + '\033[0m')
33
34    output_str_list = []
35    res = 0
36
37    if output_info:
38        res = os.system(cmd + " > output.txt 2>&1")
39    else:
40        res = os.system(cmd + " > /dev/null 2>output.txt")
41
42    with open("output.txt", "r") as file:
43        output_str_list = file.readlines()
44
45    for line in output_str_list:
46        print(line, end='')
47
48    os.remove("output.txt")
49
50    return output_str_list, res
51
52
53def build_bsp(bsp, scons_args='',name='default', pre_build_commands=None, post_build_command=None,build_check_result = None,bsp_build_env=None):
54    """
55    build bsp.
56
57    cd {rtt_root}
58    scons -C bsp/{bsp} --pyconfig-silent > /dev/null
59
60    cd {rtt_root}/bsp/{bsp}
61    pkgs --update > /dev/null
62    pkgs --list
63
64    cd {rtt_root}
65    scons -C bsp/{bsp} -j{nproc} {scons_args}
66
67    cd {rtt_root}/bsp/{bsp}
68    scons -c > /dev/null
69    rm -rf packages
70
71    """
72    success = True
73    # 设置环境变量
74    if bsp_build_env is not None:
75        print("Setting environment variables:")
76        for key, value in bsp_build_env.items():
77            print(f"{key}={value}")
78            os.environ[key] = value  # 设置环境变量
79    os.chdir(rtt_root)
80    os.makedirs(f'{rtt_root}/output/bsp/{bsp}', exist_ok=True)
81    if os.path.exists(f"{rtt_root}/bsp/{bsp}/Kconfig"):
82        os.chdir(rtt_root)
83        run_cmd(f'scons -C bsp/{bsp} --pyconfig-silent', output_info=True)
84
85        os.chdir(f'{rtt_root}/bsp/{bsp}')
86        run_cmd('pkgs --update-force', output_info=True)
87        run_cmd('pkgs --list')
88
89        nproc = multiprocessing.cpu_count()
90        if pre_build_commands is not None:
91            print("Pre-build commands:")
92            print(pre_build_commands)
93            for command in pre_build_commands:
94                print(command)
95                output, returncode = run_cmd(command, output_info=True)
96                print(output)
97                if returncode != 0:
98                    print(f"Pre-build command failed: {command}")
99                    print(output)
100        os.chdir(rtt_root)
101        # scons 编译命令
102        cmd = f'scons -C bsp/{bsp} -j{nproc} {scons_args}' # --debug=time for debug time
103        output, res = run_cmd(cmd, output_info=True)
104        if build_check_result is not None:
105            if res != 0 or not check_output(output, build_check_result):
106                    print("Build failed or build check result not found")
107                    print(output)
108        if res != 0:
109            success = False
110        else:
111            #拷贝当前的文件夹下面的所有以elf结尾的文件拷贝到rt-thread/output文件夹下
112            import glob
113            # 拷贝编译生成的文件到output目录,文件拓展为 elf,bin,hex
114            for file_type in ['*.elf', '*.bin', '*.hex']:
115                files = glob.glob(f'{rtt_root}/bsp/{bsp}/{file_type}')
116                for file in files:
117                    shutil.copy(file, f'{rtt_root}/output/bsp/{bsp}/{name.replace("/", "_")}.{file_type[2:]}')
118
119    os.chdir(f'{rtt_root}/bsp/{bsp}')
120    if post_build_command is not None:
121        for command in post_build_command:
122            output, returncode = run_cmd(command, output_info=True)
123            print(output)
124            if returncode != 0:
125                print(f"Post-build command failed: {command}")
126                print(output)
127    run_cmd('scons -c', output_info=False)
128
129    return success
130
131
132def append_file(source_file, destination_file):
133    """
134    append file to another file.
135    """
136    with open(source_file, 'r') as source:
137        with open(destination_file, 'a') as destination:
138            for line in source:
139                destination.write(line)
140
141def check_scons_args(file_path):
142    args = []
143    with open(file_path, 'r') as file:
144        for line in file:
145            match = re.search(r'#\s*scons:\s*(.*)', line)
146            if match:
147                args.append(match.group(1).strip())
148    return ' '.join(args)
149
150def get_details_and_dependencies(details, projects, seen=None):
151    if seen is None:
152        seen = set()
153    detail_list = []
154    if details is not None:
155        for dep in details:
156            if dep not in seen:
157                dep_details=projects.get(dep)
158                seen.add(dep)
159                if dep_details is not None:
160                    if dep_details.get('depends') is not None:
161                        detail_temp=get_details_and_dependencies(dep_details.get('depends'), projects, seen)
162                        for line in detail_temp:
163                            detail_list.append(line)
164                    if dep_details.get('kconfig') is not None:
165                        for line in dep_details.get('kconfig'):
166                            detail_list.append(line)
167            else:
168                print(f"::error::There are some problems with attachconfig depend: {dep}");
169    return detail_list
170
171def build_bsp_attachconfig(bsp, attach_file):
172    """
173    build bsp with attach config.
174
175    cp bsp/{bsp}/.config bsp/{bsp}/.config.origin
176    cat .ci/attachconfig/{attach_file} >> bsp/{bsp}/.config
177
178    build_bsp()
179
180    cp bsp/{bsp}/.config.origin bsp/{bsp}/.config
181    rm bsp/{bsp}/.config.origin
182
183    """
184    config_file = os.path.join(rtt_root, 'bsp', bsp, '.config')
185    config_bacakup = config_file+'.origin'
186    shutil.copyfile(config_file, config_bacakup)
187
188    attachconfig_dir = os.path.join(rtt_root, 'bsp', bsp, '.ci/attachconfig')
189    attach_path = os.path.join(attachconfig_dir, attach_file)
190
191    append_file(attach_path, config_file)
192
193    scons_args = check_scons_args(attach_path)
194
195    res = build_bsp(bsp, scons_args,name=attach_file)
196
197    shutil.copyfile(config_bacakup, config_file)
198    os.remove(config_bacakup)
199
200    return res
201
202def check_output(output, check_string):
203    """检查输出中是否包含指定字符串"""
204    output_str = ''.join(output) if isinstance(output, list) else str(output)
205    flag = check_string in output_str
206    if flag == True:
207        print('Success: find string ' + check_string)
208    else:
209        print(output)
210        print(f"::error:: can not find string {check_string}  output: {output_str}")
211
212    return flag
213if __name__ == "__main__":
214    """
215    build all bsp and attach config.
216
217    1. build all bsp.
218    2. build all bsp with attach config.
219
220    """
221    failed = 0
222    count = 0
223    ci_build_run_flag = False
224    qemu_timeout_second = 50
225
226    rtt_root = os.getcwd()
227    srtt_bsp = os.getenv('SRTT_BSP').split(',')
228    print(srtt_bsp)
229    for bsp in srtt_bsp:
230        count += 1
231        print(f"::group::Compiling BSP: =={count}=== {bsp} ====")
232        res = build_bsp(bsp)
233        if not res:
234            print(f"::error::build {bsp} failed")
235            add_summary(f"- ❌ build {bsp} failed.")
236            failed += 1
237        else:
238            add_summary(f'- ✅ build {bsp} success.')
239        print("::endgroup::")
240
241        yml_files_content = []
242        directory = os.path.join(rtt_root, 'bsp', bsp, '.ci/attachconfig')
243        if os.path.exists(directory):
244            for root, dirs, files in os.walk(directory):
245                for filename in files:
246                    if filename.endswith('attachconfig.yml'):
247                        file_path = os.path.join(root, filename)
248                        if os.path.exists(file_path):
249                            try:
250                                with open(file_path, 'r') as file:
251                                    content = yaml.safe_load(file)
252                                    if content is None:
253                                        continue
254                                    yml_files_content.append(content)
255                            except yaml.YAMLError as e:
256                                print(f"::error::Error parsing YAML file: {e}")
257                                continue
258                            except Exception as e:
259                                print(f"::error::Error reading file: {e}")
260                                continue
261
262        config_file = os.path.join(rtt_root, 'bsp', bsp, '.config')
263        # 在使用 pre_build_commands 之前,确保它被定义
264        pre_build_commands = None
265        build_command = None
266        post_build_command = None
267        qemu_command = None
268        build_check_result = None
269        commands = None
270        check_result = None
271        bsp_build_env = None
272        for projects in yml_files_content:
273            for name, details in projects.items():
274                # 如果是bsp_board_info,读取基本的信息
275                if(name == 'bsp_board_info'):
276                    print(details)
277                    pre_build_commands = details.get("pre_build").splitlines()
278                    build_command = details.get("build_cmd").splitlines()
279                    post_build_command = details.get("post_build").splitlines()
280                    qemu_command = details.get("run_cmd")
281
282                if details.get("kconfig") is not None:
283                    if details.get("buildcheckresult")  is not None:
284                        build_check_result = details.get("buildcheckresult")
285                    else:
286                        build_check_result = None
287                    if details.get("msh_cmd") is not None:
288                        commands = details.get("msh_cmd").splitlines()
289                    else:
290                        msh_cmd = None
291                    if details.get("checkresult") is not None:
292                        check_result = details.get("checkresult")
293                    else:
294                        check_result = None
295                    if details.get("ci_build_run_flag") is not None:
296                        ci_build_run_flag = details.get("ci_build_run_flag")
297                    else:
298                        ci_build_run_flag = None
299                    if details.get("pre_build") is not None:
300                        pre_build_commands = details.get("pre_build").splitlines()
301                    if details.get("env") is not None:
302                        bsp_build_env = details.get("env")
303                    else:
304                        bsp_build_env = None
305                    if details.get("build_cmd") is not None:
306                        build_command = details.get("build_cmd").splitlines()
307                    else:
308                        build_command = None
309                    if details.get("post_build") is not None:
310                        post_build_command = details.get("post_build").splitlines()
311                    if details.get("run_cmd") is not None:
312                        qemu_command = details.get("run_cmd")
313                    else:
314                        qemu_command = None
315                count += 1
316                config_bacakup = config_file+'.origin'
317                shutil.copyfile(config_file, config_bacakup)
318                #加载yml中的配置放到.config文件
319                with open(config_file, 'a') as destination:
320                    if details.get("kconfig") is None:
321                        #如果没有Kconfig,读取下一个配置
322                        continue
323                    if(projects.get(name) is not None):
324                        # 获取Kconfig中所有的信息
325                        detail_list=get_details_and_dependencies([name],projects)
326                        for line in detail_list:
327                            destination.write(line + '\n')
328                scons_arg=[]
329                #如果配置中有scons_arg
330                if details.get('scons_arg') is not None:
331                    for line in details.get('scons_arg'):
332                        scons_arg.append(line)
333                scons_arg_str=' '.join(scons_arg) if scons_arg else ' '
334                print(f"::group::\tCompiling yml project: =={count}==={name}=scons_arg={scons_arg_str}==")
335                # #开始编译bsp
336                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)
337                if not res:
338                    print(f"::error::build {bsp} {name} failed.")
339                    add_summary(f'\t- ❌ build {bsp} {name} failed.')
340                    failed += 1
341                else:
342                    add_summary(f'\t- ✅ build {bsp} {name} success.')
343                print("::endgroup::")
344
345
346                shutil.copyfile(config_bacakup, config_file)
347                os.remove(config_bacakup)
348
349
350
351        attach_dir = os.path.join(rtt_root, 'bsp', bsp, '.ci/attachconfig')
352        attach_list = []
353        #这里是旧的文件方式
354        for root, dirs, files in os.walk(attach_dir):
355            for file in files:
356                if file.endswith('attach'):
357                    file_path = os.path.join(root, file)
358                    relative_path = os.path.relpath(file_path, attach_dir)
359                    attach_list.append(relative_path)
360
361        for attach_file in attach_list:
362            count += 1
363            print(f"::group::\tCompiling BSP: =={count}=== {bsp} {attach_file}===")
364            res = build_bsp_attachconfig(bsp, attach_file)
365            if not res:
366                print(f"::error::build {bsp} {attach_file} failed.")
367                add_summary(f'\t- ❌ build {attach_file} failed.')
368                failed += 1
369            else:
370                add_summary(f'\t- ✅ build {attach_file} success.')
371            print("::endgroup::")
372
373    exit(failed)
374