1#
2# File      : compile_commands.py
3# This file is part of RT-Thread RTOS
4# COPYRIGHT (C) 2006 - 2015, RT-Thread Development Team
5#
6#  This program is free software; you can redistribute it and/or modify
7#  it under the terms of the GNU General Public License as published by
8#  the Free Software Foundation; either version 2 of the License, or
9#  (at your option) any later version.
10#
11#  This program is distributed in the hope that it will be useful,
12#  but WITHOUT ANY WARRANTY; without even the implied warranty of
13#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#  GNU General Public License for more details.
15#
16#  You should have received a copy of the GNU General Public License along
17#  with this program; if not, write to the Free Software Foundation, Inc.,
18#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19#
20# Change Logs:
21# Date           Author       Notes
22# 2025-03-02     ZhaoCake    Create compile_commands.json without bear.
23
24import os
25import json
26import re
27from SCons.Script import *
28
29def collect_compile_info(env):
30    """收集编译命令和文件信息"""
31    print("=> Starting compile command collection")
32    compile_commands = []
33    collected_files = set()
34
35    def get_command_string(source, target, env, for_signature):
36        """从SCons获取实际的编译命令"""
37        if env.get('CCCOMSTR'):
38            return env.get('CCCOM')
39        return '${CCCOM}'
40
41    def on_compile(target, source, env):
42        """编译动作的回调函数"""
43        print(f"   Processing compilation for {len(source)} source files")
44        for src in source:
45            src_path = str(src)
46            if src_path in collected_files:
47                continue
48
49            collected_files.add(src_path)
50            directory = os.path.abspath(os.path.dirname(src_path))
51
52            # 构建编译命令
53            command = env.subst(get_command_string(source, target, env, True))
54
55            # 解析include路径
56            includes = []
57            for path in env.get('CPPPATH', []):
58                includes.append('-I' + str(path))
59
60            # 添加编译命令记录
61            entry = {
62                'directory': directory,
63                'command': f"{command} {' '.join(includes)}",
64                'file': os.path.abspath(src_path),
65                'output': str(target[0]) if target else ''
66            }
67            compile_commands.append(entry)
68            print(f"   Added compile command for: {os.path.basename(src_path)}")
69
70    return on_compile, compile_commands
71
72def generate_compile_commands(env):
73    """生成compile_commands.json"""
74    print("=> Enabling compile commands generation...")
75
76    # 获取输出路径并存储到环境变量
77    output_path = GetOption('compile-commands-out') or 'compile_commands.json'
78    env['COMPILE_COMMANDS_OUT'] = output_path
79    print(f"   Compile commands will be written to: {os.path.abspath(output_path)}")
80
81    # 注册编译回调并保存到环境变量
82    callback, compile_commands = collect_compile_info(env)
83    env['COMPILE_COMMANDS'] = compile_commands
84    env.AddPreAction('*.o', callback)
85    print("   Registered compile command collector")
86
87    # 定义后处理动作
88    def write_compile_commands(target, source, env):
89        print("\n=> [DEBUG] Entering write_compile_commands callback")
90        print(f"   Target: {target}")
91        print(f"   Source: {source}")
92
93        output_path = env.get('COMPILE_COMMANDS_OUT', 'compile_commands.json')
94        compile_commands = env.get('COMPILE_COMMANDS', [])
95
96        try:
97            if not compile_commands:
98                print("Warning: No compile commands collected, skipping file generation")
99                return
100
101            print(f"\n=> Writing compile_commands.json ({len(compile_commands)} entries)")
102            with open(output_path, 'w') as f:
103                json.dump(compile_commands, f, indent=2)
104            print(f"=> Successfully generated: {os.path.abspath(output_path)}")
105
106        except PermissionError:
107            print(f"\nError: Permission denied when writing to {output_path}")
108            print("Please check file permissions and try again")
109        except Exception as e:
110            print(f"\nError writing compile_commands.json: {str(e)}")
111            import traceback
112            traceback.print_exc()
113
114    # 使用None作为目标以确保总是执行
115    print("=> Adding post-build action for compile_commands generation")
116    env.AddPostAction(None, write_compile_commands)
117
118def parse_compile_paths(json_path, rt_thread_root=None):
119    """解析compile_commands.json并提取RT-Thread相关的包含路径
120
121    Args:
122        json_path: compile_commands.json的路径
123        rt_thread_root: RT-Thread根目录路径,默认使用环境变量RTT_ROOT
124
125    Returns:
126        dict: 包含以下键的字典:
127            'sources': RT-Thread源文件的相对路径列表
128            'includes': RT-Thread头文件目录的相对路径列表
129    """
130    if rt_thread_root is None:
131        rt_thread_root = os.getenv('RTT_ROOT')
132        if not rt_thread_root:
133            raise ValueError("RT-Thread根目录未指定")
134
135    rt_thread_root = os.path.abspath(rt_thread_root)
136    result = {
137        'sources': set(),
138        'includes': set()
139    }
140
141    try:
142        with open(json_path, 'r') as f:
143            compile_commands = json.load(f)
144
145        for entry in compile_commands:
146            # 处理源文件
147            src_file = entry.get('file', '')
148            if src_file.startswith(rt_thread_root):
149                rel_path = os.path.relpath(src_file, rt_thread_root)
150                result['sources'].add(os.path.dirname(rel_path))
151
152            # 处理包含路径
153            command = entry.get('command', '')
154            include_paths = [p[2:] for p in command.split() if p.startswith('-I')]
155
156            for inc_path in include_paths:
157                if inc_path.startswith(rt_thread_root):
158                    rel_path = os.path.relpath(inc_path, rt_thread_root)
159                    result['includes'].add(rel_path)
160
161        # 转换为排序列表
162        result['sources'] = sorted(list(result['sources']))
163        result['includes'] = sorted(list(result['includes']))
164
165        return result
166
167    except Exception as e:
168        print(f"Error parsing compile_commands.json: {str(e)}")
169        return None
170
171def get_minimal_dist_paths(json_path=None, rt_thread_root=None):
172    """获取最小化发布所需的路径
173
174    Args:
175        json_path: compile_commands.json的路径,默认为当前目录下的compile_commands.json
176        rt_thread_root: RT-Thread根目录路径
177
178    Returns:
179        list: 需要包含在发布包中的相对路径列表
180    """
181    if json_path is None:
182        json_path = 'compile_commands.json'
183
184    paths = parse_compile_paths(json_path, rt_thread_root)
185    if not paths:
186        return []
187
188    # 合并源码和头文件路径
189    all_paths = set(paths['sources']) | set(paths['includes'])
190
191    return sorted(list(all_paths))
192